This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

### 3267. Rebound allocators and `is_always_equal`

Section: 16.4.4.6 [allocator.requirements] Status: New Submitter: FrankHB1989 Opened: 2019-08-27 Last modified: 2020-09-06

Priority: 4

View other active issues in [allocator.requirements].

View all other issues in [allocator.requirements].

View all issues with New status.

Discussion:

[allocator.requirements] does not mention the interaction between `is_always_equal` and allocator rebinding. As the result, a rebound allocator may have different `is_always_equal::value` to the original allocator.

Further, for an allocator type `X` satisfying `std::allocator_type<X>::is_always_equal::value == true`, rebound allocators of `X` with same type are not guaranteed equal.

Consider:

1. `X` is used as an allocator for `value_type` used in a node-based container;

2. `Y` is the rebound allocator type for the node type used in the implementation;

3. `b1` and `b2` are values of `Y` from different allocator objects.

Then, `std::allocator_type<X>::is_always_equal::value == true` does not necessarily imply `b1 == b2`.

Since some of containers in the standard have already explicitly relied on `is_always_equal` of allocators for their `value_type` (notably, in the exception specification of the move assignment), this can cause subtle problems.

In general, the implementation of the move assignment operator of such a container can not avoid allocation for new nodes when `!std::allocator_traits<Y>::propagate_on_container_move_assignment::value && b1 != b2`. This can throw, and it can clash with the required exception specification based on `std::allocator_traits<value_type>::is_always_equal`:

>     #include <utility>
>     #include <memory>
>     #include <new>
>     #include <map>
>     #include <functional>
>     #include <type_traits>

>     using K = int;
>     using V = int;
>     using P = std::pair<const K, V>;

>     bool stop_alloc;

>     template<typename T>
>     struct AT
>     {
>       using value_type = T;

>       std::shared_ptr<void> sp = {};

>       template<typename U>
>       struct rebind
>       {
>         using other = AT<U>;
>       };

>       using is_always_equal = std::is_same<T, P>;

>       AT() : sp(is_always_equal::value ? nullptr : new T*()) {}

>       AT(const AT& a) = default;

>       template<typename U>
>       AT(const AT<U>& a) noexcept : sp(a.sp) {}

>       T* allocate(std::size_t size)
>       {
>         if (stop_alloc)
>           throw std::bad_alloc();
>         return static_cast<T*>(::operator new(size * sizeof(T)));
>       }

>       void deallocate(T* p, std::size_t)
>       {
>         ::operator delete(p);
>       }

>       friend bool operator==(const AT& x, const AT& y) noexcept
>       {
>         return !x.sp.owner_before(y.sp) && !y.sp.owner_before(x.sp);
>       }

>       friend bool operator!=(const AT& x, const AT& y) noexcept
>       {
>         return !(x == y);
>       }

>     };

>     using A = AT<P>;

>     int main()
>     {
>       // Some sanity checks:
>       static_assert(std::is_same_v<A::template rebind<A::value_type>::other, A>);
>       // For any U:
>       using U = int;
>       static_assert(std::is_same_v<A::template rebind<U>::other::template rebind<A::value_type>::other, A>);

>       using C = std::less<>;
>       using M = std::map<K, V, C, A>;

>       // As required by the current wording of the container move operator:
>       using always_equal = std::allocator_traits<A>::is_always_equal;
>       constexpr bool std_nothrow = always_equal::value && std::is_nothrow_move_assignable_v<C>;
>       static_assert(std_nothrow);

>       // For conforming implementations:
>       // static_assert(!(std_nothrow && !std::is_nothrow_move_assignable<M>::value));

>       M m{{K(), V()}}, m2;
>       auto a = m.get_allocator();

>       a.sp = std::make_shared<int>(42);
>       stop_alloc = true;

>       try
>       {
>         // Call terminate with conforming implementations. This does not work on libstdc++.
>         m2 = std::move(m);
>         // For libstdc++, terminate on allocator-extended move constructor call.
>         //    M m3(std::move(m), a);
>       }
>       catch(...)
>       {}
>     }

[2019-10 Priority set to 4 after reflector discussion]

Proposed resolution:

This wording is relative to N4830.

> [Drafting note: Additional questions: Is it necessary to ensure that
> `XX::propagate_on_container_copy_assignment::value == YY::propagate_on_container_copy_assignment::value` is `true` as well?]

1. Modify 16.4.4.6 [allocator.requirements], Table [tab:cpp17.allocator] "`Cpp17Allocator` requirements" as indicated:

   > Table 34 — `Cpp17Allocator` requirements [tab:cpp17.allocator]
   > Expression | Return type | Assertion/note pre-/post-condition | Default
   > --- | --- | --- | ---
   > `…`
   > `typename X::template rebind<U>::other` | `Y` | For all `U` (including `T`), `Y::template rebind<T>::other` is `X`. `XX::is_always_equal::value == YY::is_always_equal::value` is `true`. | See Note A, below.
   > `…`