Author: | David Abrahams, Rani Sharoni, Doug Gregor |
---|---|
Contact: | dave@boostpro.com, rani_sharoni@hotmail.com, doug.gregor@gmail.com |
Organization: | BoostPro Computing |
Date: | 2009-11-09 |
Number: | N2983=09-0173 |
index
In N2855, Doug Gregor and Dave Abrahams discussed a problematic interaction between move constructors, templates, and certain standard library member functions. To date, attempts to solve the problem (including N2855 itself) have taken one basic approach: ban throwing move constructors, and be sure never to generate one.
Consider, for a moment, the actual magnitude of the problem we're addressing: it's a backward-compatibility/code evolution issue that only arises when all these conditions are satisfied:
In light of the extremely narrow set of circumstances under which the problem can arise, it seems rather heavy-handed to ban throwing move constructors altogether:
Fortunately, there is a better way. Instead of imposing this burden on every class author, we can deal with the issue more selectively in the operation being move-enabled. There, we know whether a throwing move can disturb existing guarantees. We propose that instead of using std::move(x) in those cases, thus granting permission for the compiler to use any available move constructor, maintainers of these particular operations should use std::move_if_noexcept(x), which grants permission move unless it could throw and the type is copyable. Unless x is a move-only type, or is known to have a nonthrowing move constructor, the operation would fall back to copying x, just as though x had never acquired a move constructor at all.
For example, std::pair's move constructor, were it to be written out manually, could remain as it was before this issue was discovered:
template <class First2, Second2> pair( pair<First2,Second2>&& rhs ) : first( move(rhs.first) ), second( move(rhs.second) ) {}
However, std::vector::reserve could be move-enabled this way:5
void reserve(size_type n) { if (n > this->capacity()) { pointer new_begin = this->allocate( n ); size_type s = this->size(), i = 0; try { for (;i < s; ++i) new ((void*)(new_begin + i)) value_type( std::move_if_noexcept( (*this)[i]) ) ); } catch(...) { while (i > 0) // clean up new elements (new_begin + --i)->~value_type(); this->deallocate( new_begin ); // release storage throw; } // -------- irreversible mutation starts here ----------- this->deallocate( this->begin_ ); this->begin_ = new_begin; this->end_ = new_begin + s; this->cap_ = new_begin + n; } }
We stress again that the use of std::move_if_noexcept as opposed to move would only be necessary under an extremely limited set of circumstances. In particular, it would never be required in new code, which could simply give a conditional strong guarantee, e.g. “if an exception is thrown other than by T's move constructor, there are no effects.” We recommend that approach as best practice for new code.
One possible implementation of std::move_if_noexcept might be:
template <class T> typename conditional< !nothrow_move_constructible<T>::value && has_copy_constructor<T>::value, T const&, T&& >::type move_if_noexcept(T& x) { return std::move(x); }
We propose that nothrow_move_constructible<T> be a conservative trait very much like has_nothrow_copy_constructor<T> from the current working draft; it would be identical to the proposed is_nothrow_constructible<T,T&&> from N2953. In other words, it returns true only when it can prove the move constructor doesn't throw, and returns false otherwise, even if the move constructor is actually nonthrowing.
To help the library deduce the correct result for these traits, we propose to add a new kind of exception-specification, spelled:
noexcept( integral constant expression )
The only impact of such an exception-specification is this: if a function decorated with noexcept(true) throws an exception, the behavior is undefined.3 That effect is sufficient to allow these xxx_nothrow_xxx traits to report true for any operation decorated with noexcept(true). Class maintainers could label their move constructors noexcept(true) to indicate non-throwing behavior, and the library is permitted to take advantage of that labeling if it can be detected (via “compiler magic”).
Note that the usefulness of noexcept(true) as an optimization hint goes way beyond the narrow case introduced by N2855. In fact, it goes beyond move construction: when the compiler can detect non-throwing operations with certainty, it can optimize away a great deal of code and/or data that is devoted to exception handling. Some compilers already do that for throw() specifications, but since those incur the overhead of an implicit try/catch block to handle unexpected exceptions, the benefits are limited.
The advantage of the integral constant expression parameter is that one can easily offer accurate hints in templated move constructors. For example, std::pair's converting move constructor could be written as follows:
template <class First2, Second2> pair( pair<First2,Second2>&& rhs ) noexcept( is_nothrow_constructible<First,First2&&>::value && is_nothrow_constructible<Second,Second2&&>::value )4 : first( move(rhs.first) ), second( move(rhs.second) ) {}
Although the above is reminiscent of the enable_if clause that would be required if there is a ban on throwing move constructors, the exception specification above is entirely optional; its presence or absence doesn't affect the correctness of a move constructor.
Since the common case for noexcept is to label certain operations as never throwing exceptions (without the need for a condition), the exception-specification noexcept is provided as a shorthand for noexcept(true).
It seems that has_nothrow_xxx traits are proliferating (and not just in this proposal). Once we have noexcept(bool-constant-expr) available to make the information available, it makes sense to generalize the traits into an operator similar to sizeof and typeof that can give us answers about any expression. The new operator noexcept(expression) determines whether the given expression can throw.
The noexcept operator is conservative, and will only evaluate true when the compiler can be certain that the expression will not throw, because no subexpression can throw and there are no calls to any functions that allow exceptions. Note that the wording in this document does not give compilers freedom to perform any additional analysis to determine whether a function can throw. For example, noexcept(f()) will evaluate false given the following function f, even though a sufficiently smart compiler could determine that f does not throw:
float get_float(); void f() { float x = get_float(); if (sqrt(fabs(x)) < 0) throw x; }
The generation of default move constructors, first proposed by Bjarne Stroustrup in N2904, and again by Bjarne Stroustrup and Lawrence Crowl in N2953, is harmonious with our proposal. For example, since throwing move constructors are allowed, default move constructors will be generated in more cases, with performance benefits if any subobjects have been move-enabled. A default move constructor should gain a noexcept specification whose boolean constant parameter is computed from the results of the noexcept operator for the move of all subobjects.
The proposed [[nothrow]] attribute is just a less-powerful version of this feature. In particular, it can't express the hint shown for pair's move constructor above. We suggest it be dropped.
The Microsoft compiler has always treated empty exception-specifications as though they have the same meaning we propose for noexcept(true). That is, Microsoft omits the standard-mandated runtime behavior if the function throws, and it performs optimizations based on the assumption that the function doesn't throw. This interpretation of throw() has proven to be successful in practice and is regarded by many as superior to the one in the standard. Standardizing noexcept(true) gives everyone access to this optimization tool.
So few destructors can throw exceptions that the default exception-specification for destructors could be changed from nothing (i.e. noexcept(false)) to noexcept(true) with only a tiny amount of code breakage. Such code is already very dangerous, and where used properly, ought to be a well-known “caution area” that is reasonably easily migrated. However, we don't think this change would be appropriate for C++0x at this late date, so we're not proposing it.
Add the new noexcept keyword to Table 3 - Keywords.
Modify paragraph 3 as follows:
3 An allocation function that fails to allocate storage can invoke the currently installed new-handler function (18.6.2.3), if any. [ Note: A program-supplied allocation function can obtain the address of the currently installed new_handler using the std::set_new_handler function (18.6.2.4). -- end note ] If an allocation function declared with an empty a non-throwing exception-specification (15.4), throw(), fails to allocate storage, it shall return a null pointer. Any other allocation function that fails to allocate storage shall indicate failure only by throwing an exception of a type that would match a handler (15.3) of type std::bad_alloc (18.6.2.1).
Modify the grammar in paragraph 1 as follows:
1 Expressions with unary operators group right-to-left.
unary-expression: postfix-expression ++ cast-expression -- cast-expression unary-operator cast-expression sizeof unary-expression sizeof ( type-id ) sizeof ... ( identifier ) alignof ( type-id ) noexcept-expression new-expression delete-expression
Modify paragraph 13 as follows:
13 [Note: unless an allocation function is declared with an empty a non-throwing exception-specification (15.4), throw(), it indicates failure to allocate storage by throwing a std::bad_alloc exception (Clause 15, 18.6.2.1); it returns a non-null pointer otherwise. If the allocation function is declared with an empty a non-throwing exception-specification, throw(), it returns null to indicate failure to allocate storage and a non-null pointer otherwise. -- end note] If the allocation function returns null, initialization shall not be done, the deallocation function shall not be called, and the value of the new-expression shall be null.
(Add this new section)
1 The noexcept operator determines whether the evaluation of its operand, which is an unevaluated operand ([expr] Clause 5), can throw an exception ([except.throw]).
noexcept-expression noexcept ( expression )2 The result of the
noexcept
operator is a constant of typebool
.3 The result of the
noexcept
operator isfalse
if in an evaluated context the expression would contain
- a potentially evaluated call [Footnote: This includes implicit calls, e.g., the call to an allocation function in a new-expression. -- end footnote] to a function, member function, function pointer, or member function pointer that does not have a non-throwing exception-specification ([except.spec]),
- a potentially evaluated throw-expression ([except.throw]),
- a potentially evaluated
dynamic_cast
expressiondynamic_cast<T>(v)
, whereT
is a reference type, that requires a run-time check ([expr.dynamic.cast]), or- a potentially evaluated
typeid
expression ([expr.typeid]) applied to an expression whose type is a polymorphic class type ([class.virtual]).Otherwise, the result is true.
Modify the fifth bullet of paragraph 4 as follows:
4 A pack expansion is a sequence of tokens that names one or more parameter packs, followed by an ellipsis. The sequence of tokens is called the pattern of the expansion; its syntax depends on the context in which the expansion occurs. Pack expansions can occur in the following contexts:
- In an dynamic-exception-specification (15.4); the pattern is a type-id.
Add the following case to the list in paragraph 4:
4 Expressions of the following forms are never type-dependent (because the type of the expression cannot be dependent):
noexcept ( expression )
Modify paragraph 2 as follows:
2 Expressions of the following form are value-dependent if the unary-expression or expression is type-dependent or the type-id is dependent:
sizeof unary-expression sizeof ( type-id ) alignof ( type-id ) noexcept ( expression )
Change the following paragraphs as follows:
1 A function declaration lists exceptions that its function might directly or indirectly throw by using an exception-specification as a suffix of its declarator.
exception-specification: dynamic-exception-specification noexcept-specification dynamic-exception-specification:throw (
type-id-listopt)
type-id-list: type-id...
opt type-id-list, type-id...
opt noexcept-specification:noexcept (
constant-expression)
noexcept
In a noexcept-specification, the constant-expression, if supplied, shall be a constant expression ([expr.const]) that is contextually converted to
bool
([conv] Clause 4). A noexcept-specificationnoexcept
is equivalent tonoexcept(true)
.7 A function is said to allow an exception of type E if its dynamic-exception-specification contains a type T for which a handler of type T would be a match (15.3) for an exception of type E.
11 A function with no exception-specification , or with an exception-specification of the form
noexcept(constant-expression)
where the constant-expression yieldsfalse
, allows all exceptions. An exception-specification is non-throwing if it is of the formthrow()
,noexcept
, ornoexcept(constant-expression)
where the constant-expression yieldstrue
. A function with an empty a non-throwing exception-specification ,throw()
, does not allow any exceptions.14 In an dynamic-exception-specification, a type-id followed by an ellipsis is a pack expansion (14.6.3).
Add the following new paragraph:
15 If a function with a noexcept-specification whose constant-expression yieldstrue
throws an exception, the behavior is undefined. A noexcept-specification whose constant-expression yieldstrue
is in all other respects equivalent to the exception-specificationthrow()
. A noexcept-specification whose constant-expression yieldsfalse
is equivalent to omitting the exception-specification altogether.
Modify paragraph 1 as follows:
1 The type of a handler function to be called by unexpected() when a function attempts to throw an exception not listed in its dynamic-exception-specification.
Change Header <utility> synopsis as follows:
// 20.3.2, forward/move:
template <class T> struct identity;
template <class T> T&& forward(typename identity<T>::type&&);
template <class T> typename remove_reference<T>::type&& move(T&&);
template <class T> typename conditional<
!nothrow_move_constructible<T>::value && has_copy_constructor<T>::value,
T const&, T&&>::type move_if_noexcept(T& x);
Append the following:
template <class T> typename conditional< !nothrow_move_constructible<T>::value && has_copy_constructor<T>::value, T const&, T&&>::type move_if_noexcept(T& x);
10 Returns:
std::move(t)
template <class T> struct has_nothrow_assign;
template <class T> struct has_move_constructor;
template <class T> struct nothrow_move_constructible;
template <class T> struct has_move_assign;
template <class T> struct nothrow_move_assignable;
template <class T> struct has_copy_constructor;
template <class T> struct has_default_constructor;
template <class T> struct has_copy_assign;
template <class T> struct has_virtual_destructor;
Add entries to table 43:
Template | Condition | Preconditions |
---|---|---|
template <class T> struct has_move_constructor; | T has a move constructor (17.3.14). | T shall be a complete type. |
template <class T> struct nothrow_move_constructible; | noexcept( T( make<T>() ) ) | T shall be a complete type. |
template <class T> struct has_move_assign; | T has a move assignment operator (17.3.13). | T shall be a complete type. |
template <class T> struct nothrow_move_assignable; | noexcept( *(T*)0 = make<T> ) | T shall be a complete type. |
template <class T> struct has_copy_constructor; | T has a copy constructor (12.8). | T shall be a complete type, an array of unknown bound, or (possibly cv-qualified) void. |
template <class T> struct has_default_constructor; | T has a default constructor (12.1). | T shall be a complete type, an array of unknown bound, or (possibly cv-qualified) void. |
template <class T> struct has_copy_assign; | T has a copy assignment operator (12.8). | T shall be a complete type, an array of unknown bound, or (possibly cv-qualified) void. |
Context:
iterator insert(const_iterator position, const T& x); iterator insert(const_iterator position, T&& x); void insert(const_iterator position, size_type n, const T& x); template <class InputIterator>; void insert(const_iterator position, ; InputIterator first, InputIterator last); template <class... Args> void emplace_front(Args&&... args); template <class... Args> void emplace_back(Args&&... args); template <class... Args> iterator emplace(const_iterator position, Args&&... args); void push_front(const T& x); void push_front(T&& x); void push_back(const T& x); void push_back(T&& x);`
Change Paragraph 2 as follows:
2 Remarks: If an exception is thrown other than by the copy
constructor, move constructor, move assignment operator or
assignment operator of T there are no effects. If an exception is thrown by the move constructor of a
non-CopyConstructible T
, the effects are
unspecified.
Context:
iterator erase(const_iterator position); iterator erase(const_iterator first, const_iterator last);
Change paragraph 6 as follows:
6 Throws: Nothing unless an exception is thrown by the copy constructor, move constructor, move assignment operator or assignment operator of T.
Context:
void reserve(size_type n);
Remove paragraph 2:
2 Requires: If value_type has a move constructor, that constructor shall not throw any exceptions.
Change paragraph 3 as follows:
32 Effects: A directive that informs a vector of a
planned change in size, so that it can manage the storage
allocation accordingly. After reserve(), capacity() is
greater or equal to the argument of reserve if reallocation
happens; and equal to the previous value of capacity()
otherwise. Reallocation happens at this point if and only if the
current capacity is less than the argument of reserve(). If an
exception is thrown other than by the
move constructor of a non-CopyConstructible T
there
are no effects.
Context:
void resize(size_type sz, const T& c);
Change paragraph 13 to say:
If an exception is thrown other than
by the move constructor of a non-CopyConstructible
T
there are no effects.
Change the section as follows:
iterator insert(const_iterator position, const T& x); iterator insert(const_iterator position, T&& x); void insert(const_iterator position, size_type n, const T& x); template <class InputIterator> void insert(const_iterator position, InputIterator first, InputIterator last); template <class... Args> void emplace_back(Args&&... args); template <class... Args> iterator emplace(const_iterator position, Args&&... args); void push_back(const T& x); void push_back(T&& x);1 Requires: If value_type has a move constructor, that constructor shall not throw any exceptions.
21 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, move assignment operator, or 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-CopyConstructible
T
, the effects are unspecified.32 Complexity: The complexity is linear in the number of elements inserted plus the distance to the end of the vector.
Note to proposal reader: The strong guarantee of push_back for CopyConstructible Ts is maintained by virtue of 23.2.1 [container.requirements.general] paragraph 11.
Context:
iterator erase(const_iterator position); iterator erase(const_iterator first, const_iterator last);
Change paragraph 6 as follows:
6 Throws: Nothing unless an exception is thrown by the copy constructor, move constructor, move assignment operator, or assignment operator of T.
[1] | In Frankfurt, Dave proposed that we use the attribute syntax [[moves(subobj1,subobj2)]] for this purpose. Aside from being controversial, it's a wart regardless of the syntax used, adding a whole new mechanism just for move constructors but useless elsewhere. |
[2] | Many move-enabled operations can give the strong guarantee regardless of whether move construction throws. One example is std::list<T>::push_back. This issue affects only the narrow subset of operations that need to make multiple explicit moves from locations observable by the caller. |
[3] | In particular, we are not proposing to mandate static checking: a noexcept(true) function can call a noexcept(false) function without causing the program to become ill-formed or generating a diagnostic. Generating a diagnostic in such cases can, of course, be implemented by any compiler as a matter of QOI. |
[4] | See N2953 for a definition of is_nothrow_constructible. |
[5] | Actually reserve and other such operations can be optimized even for a type without non-throwing move constructors but with a default constructor and a non-throwing swap, by first default-constructing elements in the new array and swapping each element into place. |