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.

### 2461. Interaction between allocators and container exception safety guarantees

Section: 16.4.4.6 [allocator.requirements], 24.3.11.3 [vector.capacity], 24.3.11.5 [vector.modifiers] Status: New Submitter: dyp Opened: 2014-12-06 Last modified: 2015-06-10

Priority: 3

View other active issues in [allocator.requirements].

View all other issues in [allocator.requirements].

View all issues with New status.

Discussion:

When resizing a `vector`, the accessibility and exception specification of the value type's constructors determines whether the elements are copied or moved to the new buffer. However, the copy/move is performed via the allocator's `construct` member function, which is assumed, but not required, to call the copy/move constructor and propagate only exceptions from the value type's copy/move constructor. The issue might also affect other classes.

The current wording in N4296 relevant here is from Table 28 — "Allocator requirements" in 16.4.4.6 [allocator.requirements]:

> Table 28 — Allocator requirements
> Expression | Return type | Assertion/note pre-/post-condition | Default
> --- | --- | --- | ---
> `…`
> `a.construct(c, args)` | (not used) | Effect: Constructs an object of type `C` at `c` | `::new ((void*)c) C(forward<Args>(args)...)`
> `…`

and from 16.4.4.6 [allocator.requirements] p9:

> An allocator may constrain the types on which it can be instantiated and the arguments for which its `construct` member may be called. If a type cannot be used with a particular allocator, the allocator class or the call to `construct` may fail to instantiate.

I conclude the following from the wording:

1. The allocator is not required to call the copy constructor if the arguments (args) is a single (potentially const) lvalue of the value type. Similarly for a non-const rvalue + move constructor. See also 24.2.2.1 [container.requirements.general] p15 which seems to try to require this, but is not sufficient: That paragraph specifies the semantics of the allocator's operations, but not which constructors of the value type are used, if any.

2. The allocator may throw exceptions in addition to the exceptions propagated by the constructors of the value type; it can also propagate exceptions from constructors other than a copy/move constructor.

This leads to an issue with the wording of the exception safety guarantees for vector modifiers in 24.3.11.5 [vector.modifiers] p1:

> […]

>     void push_back(const T& x);
>     void push_back(T&& x);

> > Remarks: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid. If an exception is thrown other than by the copy constructor, move constructor, assignment operator, or move assignment operator of `T` or by any InputIterator operation there are no effects. If an exception is thrown while inserting a single element at the end and `T` is `CopyInsertable` or `is_nothrow_move_constructible<T>::value` is true, there are no effects. Otherwise, if an exception is thrown by the move constructor of a non-`CopyInsertable` `T`, the effects are unspecified.

The wording leads to the following problem: Copy and move assignment are invoked directly from `vector`. For intermediary objects (see 2164), `vector` also directly invokes the copy and move constructor of the value type. However, construction of the actual element within the buffer is invoked via the allocator abstraction. As discussed above, the allocator currently is not required to call a copy/move constructor. If `is_nothrow_move_constructible<T>::value` is `true` for some value type `T`, but the allocator uses modifying operations for `MoveInsertion` that do throw, the implementation is required to ensure that "there are no effects", even if the source buffer has been modified.

Similarly, the `vector` capacity functions specify exception safety guarantees referring to the move constructor of the value type. For example, `vector::resize` in 24.3.11.3 [vector.capacity] p14:

> Remarks: If an exception is thrown other than by the move constructor of a non-`CopyInsertable` `T` there are no effects.

The wording leads to the same issue as described above.

Code example:

>     template<class T>
>     class allocator;

>     class pot_reg_type // a type which creates
>                        // potentially registered instances
>     {
>     private:
>       friend class allocator<pot_reg_type>;
>       struct register_t {};

>       static std::set<pot_reg_type*>& get_registry()
>       {
>         static std::set<pot_reg_type*> registry;
>         return registry;
>       }
>       void enregister() noexcept(false)
>       {
>         get_registry().insert(this);
>       }
>       void deregister()
>       {
>         get_registry().erase(this);
>       }

>     public:
>       pot_reg_type(void               ) noexcept(true) {}
>       pot_reg_type(pot_reg_type const&) noexcept(true) {}
>       pot_reg_type(pot_reg_type&&     ) noexcept(true) {}

>     private:
>       pot_reg_type(register_t                     ) noexcept(false)
>       { enregister(); }
>       pot_reg_type(register_t, pot_reg_type const&) noexcept(false)
>       { enregister(); }
>       pot_reg_type(register_t, pot_reg_type&&     ) noexcept(false)
>       { enregister(); }
>     };

>     template<class T>
>     class allocator
>     {
>     public:
>       using value_type = T;

>       value_type* allocate(std::size_t p)
>       { return (value_type*) ::operator new(p); }

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

>       void construct(pot_reg_type* pos)
>       {
>         new((void*)pos) pot_reg_type((pot_reg_type::register_t()));
>       }
>       void construct(pot_reg_type* pos, pot_reg_type const& source)
>       {
>         new((void*)pos) pot_reg_type(pot_reg_type::register_t(), source);
>       }

>       template<class... Args>
>       void construct(T* p, Args&&... args)
>       {
>         new((void*)p) T(std::forward<Args>(args)...);
>       }
>     };

The `construct` member function template is only required for rebinding, which can be required e.g. to store additional debug information in the allocated memory (e.g. VS2013).

Even though the value type has an accessible and `noexcept(true)` move constructor, this allocator won't call that constructor for rvalue arguments. In any case, it does not call a constructor for which vector has formulated its requirements. An exception thrown by a constructor called by this allocator is not covered by the specification in 24.3.11.5 [vector.modifiers] and therefore is guaranteed not to have any effect on the vector object when resizing.

For an example how this might invalidate the exception safety guarantees, see this post on the std-discussion mailing list.

Another problem arises for value types whose constructors are private, but may be called by the allocator e.g. via friendship. Those value types are not `MoveConstructible` (`is_move_constructible` is false), yet they can be `MoveInsertable`. It is not possible for `vector` to create intermediary objects (see 2164) of such a type by directly using the move constructor. Current implementations of the single-element forms of `vector::insert` and `vector::emplace` do create intermediary objects by directly calling one of the value type's constructors, probably to allow inserting objects from references that alias other elements of the container. As far as I can see, Table 100 — "Sequence container requirements" in 24.2.4 [sequence.reqmts] does not require that the creation of such intermediare objects can be performed by containers using the value type's constructor directly. It is unclear to me if the allocator's construct function could be used to create those intermediary objects, given that they have not been allocated by the allocator.

Two possible solutions:

1. Add the following requirement to the `allocator_traits::construct` function: If the parameter pack `args` consists of a single parameter of the type `value_type&&`, the function may only propagate exceptions if `is_nothrow_move_constructible<value_type>::value` is `false`.

   Requiring `alloctor_traits::construct` to call a true copy/move constructor of the value type breaks `std::scoped_allocator_adapter`, as pointed out by Casey Carter in a post on the std-discussion mailing list.

2. Change vector's criterion whether to move or copy when resizing:

   Instead of testing the value type's constructors via `is_move_constructible`, check the value of `noexcept( allocator_traits<Allocator>::construct(alloc, ptr, rval) )` where `alloc` is an lvalue of type `Allocator`, `ptr` is an expression of type `allocator_traits<Allocator>::pointer` and `rval` is a non-const rvalue of type `value_type`.

A short discussion of the two solutions:

Solution 1 allows keeping `is_nothrow_move_constructible<value_type>` as the criterion for `vector` to decide between copying and moving when resizing. It restricts what can be done inside the `construct` member function of allocators, and requires implementers of allocators to pay attention to the value types used. One could conceive allocators checking the following with a `static_assert`: If the value type `is_nothrow_move_constructible`, then the constructor actually called for `MoveInsertion` within the `construct` member function is also declared as noexcept.

Solution 2 requires changing both the implementation of the default allocator (add a conditional `noexcept`) and `vector` (replace `is_move_constructible` with an allocator-targeted check). It does not impose additional restrictions on the allocator (other than 24.2.2.1 [container.requirements.general] p15), and works nicely even if the move constructor of a `MoveInsertable` type is private or deleted (the allocator might be a friend of the value type).

In both cases, an addition might be required to provide the basic exception safety guarantee. A short discussion on this topic can be found in the std-discussion mailing list. Essentially, if `allocator_traits<Allocator>::construct` throws an exception, the object may or may not have been constructed. Two solutions are mentioned in that discussion:

1. `allocator_traits<Allocator>::construct` needs to tell its caller whether or not the construction was successful, in case of an exception.

2. If `allocator_traits<Allocator>::construct` propagates an exception, it shall either not have constructed an object at the specified location, or that object shall have been destroyed (or it shall ensure otherwise that no resources are leaked).

[2015-05-23, Tomasz Kamiński comments]

Solution 1 discussed in this issue also breaks support for the `polymorphic_allocator` proposed in the part of the Library Fundamentals TS v1, in addition to already mentioned `std::scoped_allocator_adapter`. Furthermore there is unknown impact on the other user-defined state-full allocators code written in the C++11.

In addition the library resolution proposed in the LWG issues 2089 and N4462, will break the relation between the `std::allocator_trait::construct` method and copy/move constructor even for the standard `std::allocator`. As example please consider following class:

>     struct NonCopyable
>     {
>       NonCopyable() = default;
>       NonCopyable(NonCopyable const&) = delete;
>       NonCopyable(NonCopyable&&) = delete;
>     };

>     struct InitListConstructor : NonCopyable
>     {
>       InitListConstructor() = default;
>       InitListConstructor(std::initializer_list<int>);
>       operator int() const;
>     };

For the above declarations following expression are ill-formed:

>     InitListConstructor copy(std::declval<InitListConstructor const&>());
>     InitListConstructor move(std::declval<InitListConstructor&&>());

So the class is not `CopyConstructible` nor `MoveConstructible`. However the following are well formed:

>     InitListConstructor copy{std::declval<InitListConstructor const&>()};
>     InitListConstructor move{std::declval<InitListConstructor&&>()};

And will be used by `std::allocator<InitListConstructor>::construct` in case of move-insertion and copy-insertion, after appliance of the resolution proposed in mentioned papers:

> The gist of the proposed library fix is simple:

> - if `is_constructible_v<TargetType, Args...>`, use direct-nonlist-initialization

> - otherwise, use brace-initialization.

As consequence the requirement proposed in the Solution 1:

> If the parameter pack `args` consists of a single parameter of the type `value_type&&`, the function may only propagate exceptions if `is_nothrow_move_constructible<value_type>::value` is false.

Will no longer hold for the `std::allocator`.

Proposed resolution: