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.
Section: 16.4.4.6 [allocator.requirements] Status: New Submitter: FrankHB1989 Opened: 2019-08-27 Last modified: 2022-04-24
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:X is used as an allocator for value_type used in a node-based container;
Y is the rebound allocator type for the node type used in the implementation;
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]
Previous resolution [SUPERSEDED]:
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?]
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-conditionDefault … typename
X::template
rebind<U>::otherY 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.…
[2022-04-24; Daniel rebases wording on N4910]
Proposed resolution:
This wording is relative to N4910.
[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?]
Modify 16.4.4.6 [allocator.requirements] as indicated:
typename X::template rebind<U>::other-16- Result: Y
-17- Postconditions: For all U (including T), Y::template rebind<T>::other is X. XX::is_always_equal::value == YY::is_always_equal::value is true. -18- Remarks: If Allocator is a class template instantiation of the form SomeAllocator<T, Args>, where Args is zero or more type arguments, and Allocator does not supply a rebind member template, the standard allocator_traits template uses SomeAllocator<U, Args> in place of Allocator::rebind<U>::other by default. For allocator types that are not template instantiations of the above form, no default is provided. -19- [Note 1: The member class template rebind of X is effectively a typedef template. In general, if the name Allocator is bound to SomeAllocator<T>, then Allocator::rebind<U>::other is the same type as SomeAllocator<U>, where SomeAllocator<T>::value_type is T and SomeAllocator<U>::value_type is U. — end note]