6

The std::allocator_traits template defines a few constants, like propagate_on_container_copy/move_assign to let other containers know whether they should copy the allocator of a second container during a copy or move operation.

We also have propagate_on_container_swap, which specifies whether the allocator should be copied during a swap operation.

Is it really necessary for an Allocator Aware container to check for allocator_traits<A>::propagate_on_container_swap in Container::swap()? Usually, I implement swap as follows:

Container::swap(Container& other)
{
   Container tmp(std::move(other));
   other = std::move(*this);
   *this = std::move(tmp);
}

In other words, I simply implement swap in terms of move assignment. Since the move assignment operation already has to deal with allocator awareness (by checking propagate_on_container_move_assign), is it okay to implement Container::swap() like this, instead of writing a totally different swap function which explicitly checks for propagate_on_container_swap?

Siler
  • 8,976
  • 11
  • 64
  • 124
  • What if `propagate_on_container_move_assign` differs from `propagate_on_container_swap`? – Yakk - Adam Nevraumont Feb 03 '15 at 22:01
  • That's a good question... under what circumstances would that even ever be desirable to have different behavior for move vs swap? – Siler Feb 03 '15 at 22:05
  • 1
    If a container uses a sentinel node, then even move construction/assignment requires a dynamic allocation and may throw, while you can implement easily a `swap()` that doesn't dynamically allocate and doesn't throw. – T.C. Feb 04 '15 at 02:24

2 Answers2

8

It is important to draw a distinction between requirements the standard places on your code vs the requirements it places on types provided by the implementor of the std::lib.

The container requirements specify how the std::containers must behave. However you are free to write your container however you like. Unless you input your container into std::code which requires the std::container behavior, you are good to go. There are just a couple of such places. For example if you adapt your Container with std::stack, then you will have to provide standard behavior if you expect the std::stack to behave according to the standard.

Getting back to your question, if you desire your Container to have the same behavior as one of the std::containers in this regard, then you will have to check and abide by all of the propagate_on traits, and all of the other allocator requirements. This is a non-trivial task. And I am not necessarily recommending it.

The std::containers will not perform a container move construction nor move assignment during swap. They will instead swap their internal representations. They will decide (at compile-time) based on propagate_on_container_swap whether or not they will swap allocators.

If propagate_on_container_swap is true, they will swap allocators and internal representations. Also in this case, swap will be noexcept in C++1z (we hope that is C++17) and forward.

If propagate_on_container_swap is false the allocators shall not be swapped, and need not even be Swappable. However the container internals are still swapped. In this case, if the two allocators do not compare equal, the behavior is undefined.

If you keep your Container::swap as it is, and create a std::stack based on your Container, the std::stack::swap will not have standard behavior. However, it will have the behavior of your Container::swap, and if that is fine with the client of said std::stack and whatever allocator they may be using, then no harm done. std::stack::swap will not behave in mysterious ways just because you didn't rigorously follow all of the intricate details for allocators that the std::containers are required to.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 2
    So `swap` will not be `noexcept` if `propagate_on_container_swap` is false? Why is that? If you're not going do anything with the allocators in that case, then why would it affect `noexcept`ness? – Praetorian Feb 03 '15 at 23:00
  • 5
    The current draft spec says: `noexcept(propagate_on_container_swap || is_always_equal)` where `is_always_equal` is a new trait that is true if the allocators always compare equal. If both of these are false, then you get into UB if the allocators are unequal. And at that point there is no sense in promising not to throw. Anything can happen. If the allocators *are* equal, then `swap` won't throw. However in this case you can't know that they are equal until run time, and so there is no sense making a compile time promise with `noexcept`. – Howard Hinnant Feb 04 '15 at 00:14
  • 2
    Thank you for clarifying, I was looking at n4140 which doesn't list any `noexcept` spec for the various containers, and the general container requirements doesn't say anything about the `is_always_equal` trait either (because it doesn't exist in that draft :)). n4296 matches what you say exactly. – Praetorian Feb 04 '15 at 00:58
  • So, you are saying that in principle your own containers can ignore these allocator semantic policies in their own implementation (and rely on the default semantics of the copy-assignment, move-assingment and swap of the allocator implementation)? Related: https://stackoverflow.com/questions/54703727/allocator-propagation-policies-in-your-new-modern-c-containers – alfC Feb 15 '19 at 19:47
  • Sure, as long as your clients don't expect your containers to follow the std allocator policies. – Howard Hinnant Feb 15 '19 at 20:32
1

Sure, you can implement it using move-ctor and move-assignment of the container.
That's the way std::swap does it though, and there is absolutely no reason to re-write it instead of using the standard one.

Those propagate-constants allow omitting useless extra-work, and you are free t ignore the potential for optimization.
Just be aware that you are potentially doing too much work, aka your swap is potentially less efficient.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118