Hi,
I'm wondering why the exception specifications for
vector::insert and other modifiers are formulated in terms of the value/element type
T instead of in terms of allocator functions.
Referring to the latest draft in the github repository (from 2013-10-30, revising N3797), [vector.modifiers]/1:
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 by the move constructor of a non-CopyInsertable T, the effects are unspecified.
[Note this paragraph is subject to
DR2252.]
The part about assignment is clear to me, but [container.requirements.general]/3 requires the use of
allocator_traits<allocator_type>::construct to create object stored in a vector. Do the remaining requirements therefore refer to
allocator_traits<allocator_type>::construct, e.g.
"copy constructor of T" referring to a call to
allocator_traits<allocator_type>::construct with a const lvalue of type
T as a third argument?
I will try to formulate my question/concern as an example. Considering the following type:
struct some_type
{
int m;
some_type(int p) : m(p) {}
some_type(some_type const&) = delete; // irrelevant
some_type(some_type&&) noexcept;
some_type(int i, some_type&& rhs)
: m( rhs.m )
{
rhs.m = -1; // violate the strong guarantee
if(i == 42) throw "some_type -- i==42";
}
~some_type() = default;
some_type& operator=(some_type const&) = default;
some_type& operator=(some_type&&) = default;
};
I think the following type
evil_allocator is a valid allocator type, e.g. for
vector<some_type, evil_allocator>:
struct evil_allocator
{
typedef some_type value_type;
some_type *allocate(std::size_t n);
void deallocate(some_type *p, std::size_t n);
void construct(some_type* p, some_type&& r)
{
static int i = 0;
some_type temp(i++, std::move(r));
// violate the strong guarantee
if(r.m == 41) throw "evil_allocator -- r.m==41";
new((void*)p) some_type( std::move(temp) );
}
void construct(some_type* p, int i)
{
new((void*)p) some_type(i);
}
};
bool operator==(const evil_allocator&, const evil_allocator&);
bool operator!=(const evil_allocator&, const evil_allocator&);
Note:
some_type is not
CopyInsertable via
evil_allocator (nor via
std::allocator<some_type>) but this should be irrelevant. The deleted copy ctor just prohibits using copy construction, as this example is written for move construction.
The move constructor of
some_type is
noexcept;
some_type is not
DefaultInsertable but
EmplaceConstructible and
MoveInsertable for
evil_allocator; it is
CopyAssignable and
MoveAssignable.
My question/concern:If
evil_allocator is a valid allocator, and the requirements in [vector.modifiers]/1 apply to
T (
here: some_type) instead of
allocator_traits<evil_allocator>::construct, wouldn't the implementation be required to provide a strong guarantee ("there are no effects") for
vector::insert etc., even if
evil_allocator::construct or
some_type::some_type(int, some_type&&) throws an exception?
Looking into libstdc++ and performing some tests seems to indicate there's indeed no strong guarantee for this implementation and the example shown above. The check
is_nothrow_move_constructible is applied directly to the value type, ignoring the allocator.
Slightly unrelated: I guess "there are no effects" means "there are no effects for the container object", consider
istream_iterator for the range overload
vector::insert(const_iterator, InputIterator, InputIterator).
Thank you very much for any clarification.