Why does propagate_on_XXX_assignment not apply to constructors? - c++

The C++11 std::allocator_traits template is used to query an Allocator to determine whether propagate_on_copy_assignment and propagate_on_move_assignment is true. These values affect how Container types must implement copy and move assignment. If std::allocator_traits<Allocator>::propagate_on_move_assignment == true then a Container move assignment operator must move assign its internal allocator object using the allocator contained in the RHS Container object.
Presumably, the point of this is that so we can implement Allocator types that can inform the client Container whether or not a move or copy operation should require that we copy any internal state inside the Allocator.
So... why do these typedefs only apply to assignment. Why do we not have propagate_on_copy_construct and propagate_on_move_construct? If an allocator has some internal state, shouldn't the client Container object be aware of that so it knows whether or not it should copy/move the allocator?

In assignment one has two choices:
Keep your existing allocator.
Copy/move from the rhs allocator.
The propagate_on_assign traits controls the choice for the assignment operators.
For construction, choice 1 (keeping your existing allocator) is not an option. There is no existing allocator. Instead your choices are:
Construct an allocator from nothing.
Copy/move from the rhs allocator.
Copy/move from some other source.
Choice one might be considered: default construct an allocator. Choice 3 is pretty vague, but different allocator schemes can be unpredictable and it is good to give the allocator designer as much flexibility as possible: Possibly to do some things that the committee never anticipated.
In an attempt to satisfy all three choices, at least for the container copy constructor, a new allocator_traits "switch" was designed:
Alloc select_on_container_copy_construction(const Alloc& rhs);
allocator_traits will return rhs. select_on_container_copy_construction() if that expression is well-formed. Otherwise it will return rhs. And container copy constructors are required to use allocator_traits<A>:: select_on_container_copy_construction(a) when constructing the lhs allocator during copy construction.
For allocator designers who just want their allocator copied during copy construction (choice 2), they don't have to do anything.
Allocator designers who want to have the lhs allocator default constructed on container copy construction just need to write the following member function for their allocator:
Alloc select_on_container_copy_construction() const {return Alloc{};}
And if they think of something clever for choice 3, they can probably implement it using the same hook as for choice 1.
So for copy construction, there is no need for a propagate_on trait as the select_on_container_copy_construction already covers all bases.
For move construction, see the bottom of page 12 of N2982:
Note that there is no select_on_container_move_construction()
function. After some consideration, we decided that a move
construction operation for containers must run in constant-time and
not throw, as per issue 1166.
Indeed this paper, and papers preceding it (that it references) are a good source of information for the rationale behind the C++11 allocator design. This paper even references a allocator_propagate_on_copy_construction from a previous paper, which subsequently evolved into select_on_container_copy_construction().

Related

Why is `boost::container::flat_set` not `nothrow_move_constructible`?

I was writing some code that has a generic container that requires elements to be nothrow_move_constructible.
I decided to add a static_assert that enforces this, just in case.
To my surprise I can't compile now when using boost::container::flat_set.
I assumed that this was just an oversight and I need a more recent boost verison, but it seems that actually they deliberately made it not safely movable:
See docs here:
http://www.boost.org/doc/libs/1_61_0/doc/html/boost/container/flat_set.html
You can see that they did update it to use R-value references and marked swap as noexcept, but they chose not to make the move ctor noexcept.
It appears that move assignment is conditionally noexcept. The condition appears to depend on the value type and on the allocator in some way.
What could be the rationale for not being nothrow move constructible? Is it just an oversight?
If the objects within a container are not nothrow_move_constructible then it is very dangerous to take an entire set of one container and relocate it to another under certain conditions (usually involving Allocators). If two containers were not constructed with the same allocator, then it is no longer safe to move memory from one container to another (think two containers from two different memory arenas).
Digging in to the current source, both the contract and the implementation are problematic:
//! <b>Effects</b>: Move constructs a flat_map.
//! Constructs *this using x's resources.
flat_map(BOOST_RV_REF(flat_map) x)
: m_flat_tree(boost::move(x.m_flat_tree))
{ ... }
So your current expectation that it could currently by nothrow is correct. But what they are doing is probably not right.
I can only guess that they are worried that they will have to do revisit this in the future and don't want to have to weaken the nothrow contract later.

Is a std::vector<T> movable if T is not movable?

I am getting a crash when trying to move a std::vector<T> where T is clearly not movable (no move constructor/assignment operator was defined, and it contains internal pointers)
But why would the move functions of vector want to call the move functions of T? It should not be necessary.
So my question from the title: Is a std::vector<T> movable if T is not movable?
Yes, std::vector<T> is movable even if T is not movable. The left side merely takes ownership from the vector on the right, no elements are touched. (With one exception, listed in #2)
The move assignment of a vector would only call the move constructor or move assignment of T if the and their allocators compare as equal and the left side's allocator's propagate_on_container_move_assignment is false. (Note that if your move constructor might throw or doesn't exist, the copy constructor will be used instead) However, it is unlikely that you are encountering either of these.
Reworded: if propagate_on_container_move_assignment is true (it usually is), then the vector is always movable, and does so without touching individual elements. If it's false, but the allocators compare equal, the vector will be moved without touching individual elements. If it's false and the allocators compare inequal, the individual elements will be transferred. If a nothrow move assignment exists, that's used. Otherwise, if a copy constructor exists, that's used. Otherwise the throwing move assignment is used. If it's neither movable nor copiable, then that's undefined behavior.
Having no move assignment operator defined, and the class containing internal pointers, does not mean your class is not movable. In fact, it sounds like the compiler thinks it is movable. If you want to disable moves, use T(T&&) = delete; and T& operator=(T&&) =delete, but I don't recommend that. Instead, add a correctly working move constructor and move assignemnt. They tend to be easy, and quite useful.
Table 99 in [container.requirements.general] of n3797 about "Allocator-aware container requirements" says about move construction:
Requires: move construction of A shall not exit via an exception.
Where A is the allocator type. It does not require MoveInsertable for the value type.
The time complexity requirement is: "constant", btw.
std::vector typically stores some pointers plus the allocator. When move-constructing a std::vector, only pointers and the allocator have to be moved. The elements don't need to be touched.

Making use of allocators in a custom container class

I'm developing a container-like class and I would like to make use of the standard allocator infrastructure much like the standard containers. On the net I find a lot of material about how to use the std::allocator class alone, or how to define a custom allocator for standard containers, but the material about how to make generically use of an standard conforming allocator is very rare, in particular in the context of C++11, where things seem to be much easier from the point of view of who writes a custom allocator, but more complex from the container's point of view.
So my question is about how to correctly make use of a standard conforming allocator in the most generic way, specifically:
First of all, when should I design a custom container in this way? Is there a sensible performance overhead (including missing optimization opportunities) in using the default allocator instead of plain new/delete?
Do I have to explicitly call contained objects' destructors?
How do I discriminate between stateful and stateless allocators?
How to handle stateful allocators?
When (if ever) are two instances interchangeable (when can I destroy with one instance the memory allocated with another one)?
They have to be copied when the container is copied?
They can/have to be moved when the container is moved?
In the container's move constructor and move assignment operator, when can I move the pointer to allocated memory, and when do I have to allocate different memory and move the elements instead?
Are there issues about exception safety in this context?
I'm specifically interested in an answer about the C++11 world (does it change anything in C++14?)
In all answers below, I'm assuming you want to follow the rules for C++11 std-defined containers. The standard does not require you to write your custom containers this way.
First of all, when should I design a custom container in this way? Is there a sensible performance overhead (including missing
optimization opportunities) in using the default allocator instead of
plain new/delete?
One of the most common and effective uses for custom allocators is to have it allocate off of the stack, for performance reasons. If your custom container can not accept such an allocator, then your clients will not be able to perform such an optimization.
Do I have to explicitly call contained objects' destructors?
You have to explicitly call allocator_traits<allocator_type>::destroy(alloc, ptr), which in turn will either directly call the value_type's destructor, or will call the destroy member of the allocator_type.
How do I discriminate between stateful and stateless allocators?
I would not bother. Just assume the allocator is stateful.
How to handle stateful allocators?
Follow the rules laid out in C++11 very carefully. Especially those for allocator-aware containers specified in [container.requirements.general]. The rules are too numerous to list here. However I'm happy to answer specific questions on any of those rules. But step one is get a copy of the standard, and read it, at least the container requirements sections. I recommend the latest C++14 working draft for this purpose.
When (if ever) are two instances interchangeable (when can I destroy with one instance the memory allocated with another one)?
If two allocators compare equal, then either can deallocate pointers allocated from the other. Copies (either by copy construction or copy assignment) are required to compare equal.
They have to be copied when the container is copied?
Search the standard for propagate_on and select_on_container_copy_construction for the nitty gritty details. The nutshell answer is "it depends."
They can/have to be moved when the container is moved?
Have to be for move construction. Move assignment depends on propagate_on_container_move_assignment.
In the container's move constructor and move assignment operator, when can I move the pointer to allocated memory, and when do I have to allocate different memory
and move the elements instead?
The newly move constructed container should have gotten its allocator by move constructing the rhs's allocator. These two allocators are required to compare equal. So you can transfer memory ownership for all allocated memory for which your container has a valid state for that pointer being nullptr in the rhs.
The move assignment operator is arguably the most complicated: The behavior depends on propagate_on_container_move_assignment and whether or not the two allocators compare equal. A more complete description is below in my "allocator cheat sheet."
Are there issues about exception safety in this context?
Yes, tons. [allocator.requirements] lists the allocator requirements, which the container can depend on. This includes which operations can and can not throw.
You will also need to deal with the possibility that the allocator's pointer is not actually a value_type*. [allocator.requirements] is also the place to look for these details.
Good luck. This is not a beginner project. If you have more specific questions, post them to SO. To get started, go straight to the standard. I am not aware of any other authoritative source on the subject.
Here is a cheat-sheet I made for myself which describes allocator behavior, and the container's special members. It is written in English, not standard-eze. If you find any discrepancies between my cheat sheet, and the C++14 working draft, trust the working draft. One known discrepancy is that I've added noexcept specs in ways the standard has not.
Allocator behavior:
C() noexcept(is_nothrow_default_constructible<allocator_type>::value);
C(const C& c);
Gets allocator from alloc_traits::select_on_container_copy_construction(c).
C(const C& c, const allocator_type& a);
Gets allocator from a.
C(C&& c)
noexcept(is_nothrow_move_constructible<allocator_type>::value && ...);
Gets allocator from move(c.get_allocator()), transfers resources.
C(C&& c, const allocator_type& a);
Gets allocator from a. Transfers resources if a == c.get_allocator().
Move constructs from each c[i] if a != c.get_allocator().
C& operator=(const C& c);
If alloc_traits::propagate_on_container_copy_assignment::value is true,
copy assigns allocators. In this case, if allocators are not equal prior
to assignment, dumps all resources from *this.
C& operator=(C&& c)
noexcept(
allocator_type::propagate_on_container_move_assignment::value &&
is_nothrow_move_assignable<allocator_type>::value);
If alloc_traits::propagate_on_container_move_assignment::value is true,
dumps resources, move assigns allocators, and transfers resources from c.
If alloc_traits::propagate_on_container_move_assignment::value is false
and get_allocator() == c.get_allocator(), dumps resources, and transfers
resources from c.
If alloc_traits::propagate_on_container_move_assignment::value is false
and get_allocator() != c.get_allocator(), move assigns each c[i].
void swap(C& c)
noexcept(!allocator_type::propagate_on_container_swap::value ||
__is_nothrow_swappable<allocator_type>::value);
If alloc_traits::propagate_on_container_swap::value is true, swaps
allocators. Regardless, swaps resources. Undefined behavior if the
allocators are unequal and propagate_on_container_swap::value is false.

Has C++11 changed requirements for elements of STL containers, and how?

Recently I'm surprised at the fact std::unique_ptr is acceptable for elements of STL containers, because I thought these elements are required to provide functions below(this page says the same):
public default constructor with no arguments
public copy constructor
public copy assignment operator function
public destructor
But std::unique_ptr is not copyable to make the pointer it holds owned by a single object, that contradicts the requirements above.
Has the standard changed the requirements? If so, what are the changes? Perhaps either movable objects or copyable ones are sufficient?
I've searched the web whether the requirements has changed since C++11, but I can't find any page which helps me...
Yes, there have been major changes to the requirements for standard library containers. It's hard to provide a comprehensive list (there were a lot), but here are some important ones:
std::vector generally requires only that its members be MoveConstructible and MoveAssignable. There are many member functions of std::vector that impose more strict requirements. vector::push_back requires Move or CopyConstructible (depending on whether you pass an rvalue or lvalue), but the new vector::emplace_back only requires that there is an accessible constructor that takes the given parameters (in addition to the baseline requirement). Obviously any attempt to invoke the vector's copy constructor/assignment will require that the type be CopyConstructible (ie: you can't copy a std::vector<unique_ptr>).
Similarly, most other containers have reduced the restrictions on the type. They also have emplace member functions that allow you to construct the members in-place, as well as l/rvalue insertion functions. Which means that you don't have to copy values in; you can move them in or construct them in-place.
None of the constructors or destructors are required to be public; all construction takes place via calls to allocator_traits<Allocator>::construct calls. So if you provide an allocator, you could make your constructors/destructors private. Well, as long as your allocator class can access them, of course.
In short, the requirements are much less strict, but they're a bit more complex. You can get away with a lot of things if you restrict yourself from performing certain operations on the container.

Why aren't container move assignment operators noexcept?

I noticed that std::string's (really std::basic_string's) move assignment operator is noexcept. That makes sense to me. But then I noticed that none of the standard containers (e.g., std::vector, std::deque, std::list, std::map) declares its move assignment operator noexcept. That makes less sense to me. A std::vector, for example, is typically implemented as three pointers, and pointers can certainly be move-assigned without throwing an exception. Then I thought that maybe the problem is with moving the container's allocator, but std::string's have allocators, too, so if that were the issue, I'd expect it to affect std::string.
So why is std::string's move assignment operator noexcept, yet the move assignment operators for the standard containers are not?
I believe we're looking at a standards defect. The noexcept specification, if it is to be applied to the move assignment operator, is somewhat complicated. And I believe this statement to be true whether we are talking about basic_string or vector.
Based on [container.requirements.general]/p7 my English translation of what a container move assignment operator is supposed to do is:
C& operator=(C&& c)
If alloc_traits::propagate_on_container_move_assignment::value is
true, dumps resources, move assigns allocators, and transfers
resources from c.
If
alloc_traits::propagate_on_container_move_assignment::value is false
and get_allocator() == c.get_allocator(), dumps resources, and transfers
resources from c.
If
alloc_traits::propagate_on_container_move_assignment::value is false
and get_allocator() != c.get_allocator(), move assigns each c[i].
Notes:
alloc_traits refers to allocator_traits<allocator_type>.
When alloc_traits::propagate_on_container_move_assignment::value is true the move assignment operator can be specified noexcept because all it is going to is deallocate current resources and then pilfer resources from the source. Also in this case, the allocator must also be move assigned, and that move assignment must be noexcept for the container's move assignment to be noexcept.
When alloc_traits::propagate_on_container_move_assignment::value is false, and if the two allocators are equal, then it is going to do the same thing as #2. However one doesn't know if the allocators are equal until run time, so you can't base noexcept on this possibility.
When alloc_traits::propagate_on_container_move_assignment::value is false, and if the two allocators are not equal, then one has to move assign each individual element. This may involve adding capacity or nodes to the target, and thus is intrinsically noexcept(false).
So in summary:
C& operator=(C&& c)
noexcept(
alloc_traits::propagate_on_container_move_assignment::value &&
is_nothrow_move_assignable<allocator_type>::value);
And I see no dependence on C::value_type in the above spec and so I believe it should apply equally well to std::basic_string despite C++11 specifying otherwise.
Update
In the comments below Columbo correctly points out that things have gradually been changing all the time. My comments above are relative to C++11.
For the draft C++17 (which seems stable at this point) things have changed somewhat:
If alloc_traits::propagate_on_container_move_assignment::value is true, the spec now requires the move assignment of the allocator_type to not throw exceptions (17.6.3.5 [allocator.requirements]/p4). So one no longer needs to check is_nothrow_move_assignable<allocator_type>::value.
alloc_traits::is_always_equal has been added. If this is true, then one can determine at compile time that point 3 above can not throw because resources can be transferred.
So the new noexcept spec for containers could be:
C& operator=(C&& c)
noexcept(
alloc_traits::propagate_on_container_move_assignment{} ||
alloc_traits::is_always_equal{});
And, for std::allocator<T>, alloc_traits::propagate_on_container_move_assignment{} and alloc_traits::is_always_equal{} are both true.
Also now in the C++17 draft, both vector and string move assignment carry exactly this noexcept specification. However the other containers carry variations of this noexcept specification.
The safest thing to do if you care about this issue is to test explicit specializations of containers you care about. I've done exactly that for container<T> for VS, libstdc++ and libc++ here:
http://howardhinnant.github.io/container_summary.html
This survey is about a year old, but as far as I know is still valid.
I think the reasoning for this goes like this.
basic_string only works with non-array POD types. As such, their destructors must be trivial. This means that if you do a swap for move-assignment, it doesn't matter as much to you that the original contents of the moved-to string haven't been destroyed yet.
Whereas containers (basic_string is not technically a container by the C++ spec) can contain arbitrary types. Types with destructors, or types that contain objects with destructors. This means that it is more important to the user to maintain control over exactly when an object is destroyed. It specifically states that:
All existing elements of a [the moved-to object] are either move assigned to or destroyed.
So the difference does make sense. You can't make move-assignment noexcept once you start deallocating memory (through the allocator) because that can fail via exception. Thus, once you start requiring that memory is deallocated on move-assign, you give up being able to enforce noexcept.
The move assignment operator in container classes is defined to be noexcept because many containers are designed to implement the strong exception safety guarantee. Containers implement the strong exception safety guarantee because back before there were move assignment operators, the container had to be copied. If anything went wrong with the copy, the new storage was deleted and the container remained unchanged. Now we're stuck with that behavior. If the move assignment op is not noexcept, the slower copy assignment operator is invoked instead.