2501. std::function requires POCMA/POCCA

Section: 22.10.17.3 [func.wrap.func] Status: Resolved Submitter: David Krauss Opened: 2015-05-20 Last modified: 2020-09-06

Priority: 3

View all other issues in [func.wrap.func].

View all issues with Resolved status.

Discussion:

The idea behind propagate_on_container_move_assignment is that you can keep an allocator attached to a container. But it's not really designed to work with polymorphism, which introduces the condition where the current allocator is non-POCMA and the RHS of assignment, being POCMA, wants to replace it. If function were to respect the literal meaning, any would-be attached allocator is at the mercy of every assignment operation. So, std::function is inherently POCMA, and passing a non-POCMA allocator should be ill-formed.

The other alternative, and the status quo, is to ignore POCMA and assume it is true. This seems just dangerous enough to outlaw. It is, in theory, possible to properly support POCMA as far as I can see, albeit with difficulty and brittle results. It would require function to keep a throwing move constructor, which otherwise can be noexcept.

The same applies to propagate_on_container_copy_assignment. This presents more difficulty because std::allocator does not set this to true. Perhaps it should. For function to respect this would require inspecting the POCCA of the source allocator, slicing the target from the erasure of the source, slicing the allocation from the erasure of the destination, and performing a copy with the destination's allocator with the source's target. This comes out of the blue for the destination allocator, which might not support the new type anyway. Theoretically possible, but brittle and not very practical. Again, current implementations quietly ignore the issue but this isn't very clean.

The following code example is intended to demonstrate the issue here:

#include <functional>
#include <iostream>
#include <vector>

template <typename T>
struct diag_alloc 
{
  std::string name;

  T* allocate(std::size_t n) const 
  {
    std::cout << '+' << name << '\n';
    return static_cast<T*>(::operator new(n * sizeof(T)));
  }
  
  void deallocate(T* p, std::size_t) const 
  {
    std::cout << '-' << name << '\n';
    return ::operator delete(p);
  }

  template <typename U>
  operator diag_alloc<U>() const { return {name}; }

  friend bool operator==(const diag_alloc& a, const diag_alloc& b)
  { return a.name == b.name; }
  
  friend bool operator!=(const diag_alloc& a, const diag_alloc& b)
  { return a.name != b.name; }

  typedef T value_type;
  
  template <typename U>
  struct rebind { typedef diag_alloc<U> other; };
};

int main() {
  std::cout << "VECTOR\n";
  std::vector<int, diag_alloc<int>> foo({1, 2}, {"foo"}); // +foo
  std::vector<int, diag_alloc<int>> bar({3, 4}, {"bar"}); // +bar

  std::cout << "move\n";
  foo = std::move(bar); // no message

  std::cout << "more foo\n";
  foo.reserve(40); // +foo -foo
  std::cout << "more bar\n";
  bar.reserve(40); // +bar -bar

  std::cout << "\nFUNCTION\n";
  int bigdata[100];
  auto bigfun = [bigdata]{};
  typedef decltype(bigfun) ft;
  std::cout << "make fizz\n";
  std::function<void()> fizz(std::allocator_arg, diag_alloc<ft>{"fizz"}, bigfun); // +fizz
  std::cout << "another fizz\n";
  std::function<void()> fizz2;
  fizz2 = fizz; // +fizz as if POCCA
  std::cout << "make buzz\n";
  std::function<void()> buzz(std::allocator_arg, diag_alloc<ft>{"buzz"}, bigfun); // +buzz
  std::cout << "move\n";
  buzz = std::move(fizz); // -buzz as if POCMA

  std::cout << "\nCLEANUP\n";
}

[2016-08, Chicago]

Tues PM: Resolved by P0302R1.

Proposed resolution:

Resolved by P0302R1.