Section: 16.4.4.6 [allocator.requirements], 23.3.11.3 [vector.capacity], 23.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.
Table 28 — Allocator requirements Expression Return type Assertion/note
pre-/post-conditionDefault …
a.construct(c, args)
(not used) Effect: Constructs an object of type C
atc
::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 toconstruct
may fail to instantiate.
I conclude the following from the wording:
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 23.2.2 [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.
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 23.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 andT
isCopyInsertable
oris_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.
vector
capacity functions specify exception safety guarantees
referring to the move constructor of the value type. For example, vector::resize
in 23.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).
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 23.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 23.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:
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
.
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.
Change vector's criterion whether to move or copy when resizing:
Instead of testing the value type's constructors viais_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 keepingis_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
23.2.2 [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:
allocator_traits<Allocator>::construct
needs to tell its caller
whether or not the construction was successful, in case of an exception.
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.
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-initializationotherwise, use brace-initialization.
As consequence the requirement proposed in the Solution 1:
If the parameter pack
args
consists of a single parameter of the typevalue_type&&
, the function may only propagate exceptions ifis_nothrow_move_constructible<value_type>::value
is false.
Will no longer hold for the std::allocator
.
Proposed resolution: