Why does `std::pmr::polymorphic_allocator` not propagate on container move?
Asked Answered
M

1

12

From http://en.cppreference.com/w/cpp/memory/polymorphic_allocator:

polymorphic_allocator does not propagate on container copy assignment, move assignment, or swap. As a result, move assignment of a polymorphic_allocator-using container can throw, and swapping two polymorphic_allocator-using containers whose allocators do not compare equal results in undefined behavior.

Why would I ever want this behavior? Not only does this seem to introduce gratuitous undefined behavior on swap, but more importantly for my purposes, it implies that std::pmr::vector is effectively a non-move-assignable type. I mean, it's move-assignable, but that's almost guaranteed to be inefficient.

std::vector<int> v = {1, 2, 3};
std::vector<int> w;
w = std::move(v);  // nocopy, nothrow

std::pmr::monotonic_buffer_resource mr(1000);
std::pmr::vector<int> v( {1, 2, 3}, &mr );
std::pmr::vector<int> w;
w = std::move(v);  // yescopy, yesthrow

My guess is that this is a primitive attempt to deal with ownership issues. In my second example above, v holds a reference to mr but v does not actually own mr. Allowing non-owning references to propagate unchecked throughout the system would tend to introduce lots of subtle bugs. So rather than invent an owning allocator, the designers decided simply not to propagate the reference to mr. This ended up having bad effects, such as that move-assigning a vector now copies its data; but you don't end up with quite so many dangling pointers to memory resources. (Some yes, but not as many.)


P.S. I do already see that you can avoid the copying/throwing by setting up the allocator in advance, like this:

std::pmr::monotonic_buffer_resource mr(1000);
std::pmr::vector<int> v( {1, 2, 3}, &mr );
std::pmr::vector<int> w(v.get_allocator());
w = std::move(v);  // nocopy, nothrow
Mcclenon answered 13/7, 2017 at 18:10 Comment(1)
"that's almost guaranteed to be inefficient" The standard never guaranteed that any container move-assignment operation was constant time. It says so right in the requirements: move assignment is linear. Move construction is constant time, but not assignment. Generally speaking, people don't move into existing containers all that often. As such, it's not a big deal. Furthermore, the move operation will not copy the elements. It will simply move them. So it's not the same thing as saying there is no move assignment.Inductee
I
10

The allocator_traits<>::propagate_on_container_copy/move_assignment/swap are static properties of an allocator. A polymorphic allocator, by design, has its properties defined at runtime. Some polymorphic allocators can propagate to others, and some cannot.

Therefore, it is impossible for these properties to be known at compile time. So the PMR allocator must assume the worst case at compile time: no propagation.

Let's take your example, with an alteration:

std::pmr::monotonic_buffer_resource mr(1000);
std::pmr::vector<int> v( {1, 2, 3}, &mr );
std::pmr::vector<int> w;
auto a1 = w.get_allocator();
w = std::move(v);
assert(w.get_allocator() == a1);

The C++ standard requires that this does not assert. Assignment does not move the allocator of a container; that's how container assignment works with allocators in C++.

Therefore, either the allocator of the destination container can handle the memory of the source container, or it cannot. And if it cannot, then the destination container must allocate memory and copy/move the objects from the source.

Whether the move assignment operator is noexcept or not is a static property of the operator. And since PMRs cannot know until runtime whether they can propagate storage, the container must designate its move assignment operator as a throwing move assignment.

At runtime, it can actually decide whether propagation is possible or not. And if it is possible, then it will take the more efficient path.

Inductee answered 13/7, 2017 at 21:28 Comment(1)
@Nicol_Bolas I'm slightly confused by the sentence "Some polymorphic allocators can propagate to others, and some cannot." I mean polymorphic_allocator is just holding a pointer to a memory_resource to use for the allocation. My only guess is that this refers to the case when the memory resource used by the polymorphic_allocator is only intended to be used for allocation for a particular instance of say a std::pmr::vector? Is this correct?Gilgilba

© 2022 - 2024 — McMap. All rights reserved.