Authors: Douglas Gregor, David Abrahams
Contact: [email protected], [email protected]
Organization: Apple, BoostPro Computing
Date: 2009-03-23
Number: N2855=09-0045
This paper describes a problem with the move construction idiom that compromises the exception safety guarantees made by the Standard Library. In particular, well-formed C++03 programs, when compiled with the C++0x Standard Library, can no longer rely on the strong exception safety guarantee provided by certain standard library container operations. This change silently breaks existing user code. In this paper, we characterize the problem itself and outline a solution that extends the language and modifies the library.
Within the library, we characterize the behavior of a function with respect to exceptions based on the guarantees that the implementation must provide if an exception is thrown. These guarantees describe the state of the program once a thrown exception has unwound the stack past the point of that function. We recognize three levels of exception safety guarantees for a given library function:
The Standard Library provides at least the basic exception
guarantee throughout, which has not changed with the introduction of
rvalue references. However, some functions of the library provide the
strong exception guarantee, such as push_back
(23.1.1p10).
T
is T(T&&)
, but every move constructor:
T
T(std::move(x))
, where x
is an lvalue of
type T
. The std::move
function template
simply casts its lvalue argument to the corresponding rvalue type:
template <class T> T&& move(T& x) { return static_cast<T&&>(x); }The
T(std::move(x))
idiom is only a move request
because in general, T
may not have a move constructor, in
which case an available copy constructor will be used instead. Such
an explicit move request can be used anytime a function knows that a
particular value is no longer needed. The simplest example is
in std::swap
:
template <class T> void swap(T& x, T& y) { T tmp(std::move(x)); // 1 x = std::move(y); // 2 y = std::move(tmp); // 3 }In this case, it is safe to move from
x
in line 1 because
we are about to assign into x
in line 2.
Since copying x
is also an acceptable (though
sometimes suboptimal) way to transfer its value into tmp
,
this code still works when T
has a copy constructor but
no move constructor.
The act of constructing a new T
from an
explicitly-generated T&&
bound to an lvalue is known in
this paper as "the move operation," whether it actually ends up moving
or falls back to invoking a copy constructor.
The problem addressed by this paper is three-fold. First, under
C++0x, some existing types such
as pair<string,legacy_type>
automatically acquire a
throwing move constructor. Second, currently-legal operations such as
insertion into a container of those pairs will invoke undefined
behavior, because the C++0x Standard Library bans throwing move
constructors in those operations. Third, operations such
as vector
, that are currently required to
provide the strong exception guarantee, only provide the basic
guarantee when T
's move constructor can throw.
As an example,
we consider vector
's push_back
operation,
e.g.,
vec.push_back(x);
In the call to push_back
, if the size of the
vector
is the same as its capacity, we will have to
allocate more storage for the vector
. In this case, we
first allocate more storage and then "move" the contents from the old
storage into the new storage. Finally, we copy the new element into
the new storage and, if everything has succeeded, free the old
storage. The reallocation routine looks something like this:
T* reallocate(T *old_ptr, size_t old_capacity) { // #1: allocate new storage T* new_ptr = (T*)new char[sizeof(T) * old_capacity * 2]; // #2: try to move the elements to the new storage unsigned i = 0; try { // #2a: construct each element in the new storage from the corresponding // element in the old storage, treating the old elements as rvalues for (; i < old_capacity; ++i) new (new_ptr + i) T(std::move(old_ptr[i])); // "move" operation } catch (...) { // #2b: destroy the copies and deallocate the new storage for (unsigned v = 0; v < i; ++v) new_ptr[v]->~T(); delete[]((char*)new_ptr); throw; } // #3: free the old storage for (i = 0; i < old_capacity; ++i) old_ptr[i]->~T(); delete[]((char*)old_ptr); return new_ptr; }
For this discussion we are interested in section #2, which handles
the movement of values from the old storage to the new storage. The
use of std::move
treats the elements in the old storage
as rvalues, enabling a move constructor (if available) or falling back
to a copy constructor (if no move constructor is available) with the
same syntax.
Consider reallocation of the vector when the type stored in the vector provides only a copy constructor (and no move constructor), as shown below. Here, we copy elements from the old storage (top) to the new storage (bottom).
|
||||||||||||||||
|
||||||||||||||||
|
While copying the fifth element (e
) the copy constructor throws an exception. At this point, we can still recover, since the old storage still contains the original (unmodified) data. Thus, the recovery code (section #2b) destroys the elements in the new storage and then frees the new storage, providing the strong exception safety guarantee.
When the type stored in the vector provides a move constructor, each of the values is moved from the old storage into the new storage, potentially mutating the values in the old storage. The notion is shown below, half-way through the reallocation:
|
||||||||||||||||
|
||||||||||||||||
|
When the element's move constructor cannot throw, the initialization of the new storage is guaranteed to succeed, since no operations after the initial memory allocation can throw.
However, if the element's move constructor can throw an exception,
that exception can occur after the vector's state has been altered
(say, while moving the value e
), and there's no reliable
way to recover the vector's original state, since any attempt to move
the previously-moved elements back into the vector's storage could
also throw. Hence, the best we can do is maintain the basic
guarantee, where no resources are leaked but the first four elements in
the vector have indeterminate values.
Stepping back from this specific instance, we can formulate a simple model for achieving the strong exception-safety guarantee. In this model, we take the set of operations that we need to perform in our routine and partition them into two sets: those operations that perform nonreversible modifications to existing data and those operations that can throw exceptions. Providing strong exception safety means placing any operations that can throw exceptions (memory allocation, copying, etc.) before any operations that perform nonreversible modifications to existing data (destroying an object, freeing memory).
Reconsidering vector
reallocation in terms of this
model, we see that, if we ignore throwing move constructors, the
implementation of reallocate
performs all of its
possibly-throwing routines up front: we allocate memory, then copy
(which may throw) or move (which won't throw), then we complete the
operation. Either way, at some point within the routine we have
committed to only using operations that can no longer throw, such as
deallocating memory or destroying already-constructed objects.
The problem with a throwing move constructor is that it fits into both partitions. It can throw exceptions (obviously) and it is also a non-reversible modification, because (1) moving is permitted to transfer resources and (2) there is no non-throwing operation to reverse the transfer of resources.
Based on this model, prohibiting the use of types that have throwing move constructors appears to solve the problem. It does help, but we'll need to go further than that to prevent the generation of throwing move constructors in standard library class templates.
So, how easy is it to violate the requirement that move
constructors not throw exceptions? It turns out to be effortless. In
fact existing, well-formed C++03 programs will violate this
requirement when compiled with the C++0x Standard Library because the
standard library itself creates throwing move constructors. As an
example, consider a simple Matrix
type that stores its
values on the heap:
class Matrix { double *data; unsigned rows, cols; public: Matrix(const Matrix& other) : rows(other.rows), cols(other.cols) { data = new double [rows * cols]; // copy data... } };
The Matrix
type has a copy constructor that can throw an exception, but it has no move constructors. A vector
of Matrix
values is certainly well-formed and its push_back
provides the strong exception safety guarantee. This is true both in C++03 and in C++0x.
Next, we compose a std::string
with a Matrix
using std::pair
:
typedef std::pair<std::string, Matrix> NamedMatrix;
Consider std::pair
's move constructor, which will look something like this (simplified!):
template<typename T, typename U> struct pair { pair(pair&& other) : first(std::move(other.first)), second(std::move(other.second)) { } T first; U second; };
Here, the pair
's first
data member is
a std::string
, which has a non-throwing move constructor
that modifies its source
value. The pair
's second
data member is
a Matrix
, which has a throwing copy constructor but no
move constructor. When we compose these two types, we end up with a
type—std::pair<std::string, Matrix>
—that merges
their behaviors. This pair
's move constructor performs a
non-reversible modification on the first
member of the
pair (moving the resources of the std::string
) and then
performs a potentially-throwing copy construction on
the second
member of the pair (copying
the Matrix
). Thus, we have composed two well-behaved
types, one from the library and one from user code, into a type that
violates the prohibition on throwing move constructors. Moreover, this
problem affects valid C++03 code, which will silently invoke undefined
behavior when compiled with a C++0x Standard Library
Given the prohibition on throwing move
constructors, pair
should only declare a move constructor
when it is guaranteed that the underlying move operations for the
types it aggregates are both non-throwing. Using concept syntax, one
might imagine that such a constructor would be written as:
requires NothrowMoveConstructible<T> && NothrowMoveConstructible<U> pair(pair&∓ other) : first(std::move(other.first)), second(std::move(other.second)) { }
In this case, pair
will only provide a move
constructor when that move constructor is guaranteed to be
non-throwing. Therefore, std::pair<std::string,
Matrix>
will not provide a move constructor, and reallocating
a vector
of these pairs will use the copy constructor,
maintaining the strong exception safety
guarantee. Naturally, pair
is not the only type whose
move constructor is affected: any standard library and user type that
aggregates other values, including tuples and containers, will need
similarly-constrained move constructors.
At present, there is no satisfactory way to write
the NothrowMoveConstructible
concept. Using C++0x as
currently specified, we could try to write the new concept as
follows:
concept NothrowMoveConstructible<typename T, typename U = T> { requires RvalueOf<U> && Constructible<T, RvalueOf<U>::type>; }
The important aspect of this concept is that it is not an
auto
concept, so clients are required to "opt in" by
explicitly stating, via a concept map, that they provide a
non-throwing move constructor. The library would provide concept maps
for its own types, e.g.
concept_map NothrowMoveConstructible<string> { }
Some of these concept maps will, naturally, be conditional on their
inputs. For example, pair
's concept map can be expressed
as follows:
template<typename T1, typename T2, typename U, typename V> requires NothrowMoveConstructible<T1, U> && NothrowMoveConstructible<T2, V> concept_map NothrowMoveConstructible<pair<T1, T2>, pair<U, V>> { }
If diligently applied throughout the library and user code,
NothrowMoveConstructible
permits the safe use of
move semantics within the library, retaining the strong exception
safety guarantee.
The danger with a library-only solution is that it is far too easy
for users of the language to accidentally write a move constructor
that can throw exceptions (see std::pair
), and a single
class or class template that makes such a mistake compromises the
exception safety guarantees of the library. The concepts system cannot
protect the user from such a mistake, because there is no way to
statically determine whether a function can throw exceptions. Even if
concepts could prevent such an error in the library, non-templated and
unconstrained templates would still be susceptible to this class of
errors. To address these problems, we propose to introduce language
facilities that allow the non-throwing guarantee to be declared for
functions, statically enforced by the compiler, and queried by
concepts.
noexcept
SpecifierWe propose the addition of a new declaration specifier,
noexcept
, that indicates that the function it applies to
does not throw any exceptions. The noexcept
specifier can
only be applied to function declarators, e.g.,
noexcept int printf(const char* format, ...); // okay: printf does not throw exceptions. noexcept int (*funcptr)(const char*, ...); // okay: pointer to a function that does not throw exceptions noexcept int x; // error: not a function declarator
The noexcept
specifier differs from an empty exception
specification (spelled throw()
) in two important
ways. First, a noexcept
function is ill-formed if it (or
any function it calls) may throw an exception, e.g.,
noexcept int foo(int); int bar(int); noexcept void wibble(int x, int y) { x = foo(x); // okay: foo can not throw any exceptions y = bar(y); // error: bar() could throw an exception try { y = bar(y); } catch (...) { y = 0; } // okay: all exceptions that could be thrown have been captured }
Second, the presence or absence of the noexcept
specifier is part of a function type. Thus, the types of the function
pointers fp1
and fp2
, shown below, are
distinct:
noexcept int (*fp1)(int); // okay: pointer to a function that does not throw exceptions int (*fp2)(int); // okay: pointer to a function that may throw exceptions
There is an implicit conversion from pointers and references
to noexcept
pointers to their potentially-throwing
equivalents. For example:
noexcept int f(int); int g(int); noexcept int (*fp1)(int) = &f; // okay: exact match int (*fp2)(int) = &g; // okay: exact match int (*fp3)(int) = &f; // okay: conversion from pointer tonoexcept
function to a pointer to a non-noexcept
function noexcept int (*fp4)(int) = &g; // error: no conversion from a pointer to a throwing function type to a pointer to a non-throwing function type
In short, noexcept
provides the behavior that
many users expect from throw()
. That exception specifications
are not statically checked is a constant source of
confusion, especially for programmers who have used the similar
(statically-checked) facilities in Java. Moreover, exception
specifications have a poorly-defined role in the C++ type system,
because they can only be used in very limited ways.
Note: functions cannot be overloaded based on noexcept
alone, and noexcept
is not part of a function's
signature. For example, the following code is ill-formed:
noexcept void f() { } // #1 void f() { } // #2: redefinition of #1
noexcept
In ConceptsThe noexcept
specifier can be used on associated
functions within concepts, allowing one to detect whether a particular
operation is guaranteed not to throw exceptions. For example:
auto concept NothrowMoveConstructible<typename T, typename U = T> { requires RvalueOf<U>; noexcept T::T(RvalueOf<U>::type); }
This new, noexcept
formulation of the
NothrowMoveConstructible
concept has two benefits over
the previous formulation. First, since the noexcept
property is statically checked, there is no potential for users to
accidentally claim that no exceptions will be thrown from their move
constructors. Second, since we have strong static checking for the
no-throw policy, we have made this an auto
concept, so
that users need not write concept maps for this low-level
concept.
Types with throwing move constructors are prohibited within the standard library, and we have not seen motivating use cases for such a feature. On the other hand, failure to mark a non-throwing move constructor as noexcept
means that users will miss out on the many optimization opportunities in the standard library that depend on rvalue references.
We therefore propose that all move constructors be implicitly noexcept
. This way, users that implement move constructors can be certain that their move constructors (1) will be used by the standard library and (2) will meet the non-throwing requirements of the standard library.
Specifically, for a class X
, any constructor whose first parameter is of type X&&
and whose remaining parameters, if any, all have default arguments, is considered a move constructor. For a class tempate X
, any constructor or constructor template whose first parameter is of type X<T1, T2, ..., TN>
for any T1
, T2
, ..., TN
and whose remaining parameters, if any, all have default arguments, is considered a move constructor or move constructor template. If a constructor or constructor template is a move constructor or move constructor template, it is implicitly declared as noexcept
.
Nearly every operation that involves construction of an object also involves destruction of that object. Therefore, the vast majority of uses of a non-throwing move constructor will also require a non-throwing destructor. Non-throwing destructors are not new; in fact, the C++ Standard Library requires that destructors not throw exceptions, and it is very rare that destructors ever throw. For these reasons, we propose that all destructors be implicitly be declared noexcept
and are, therefore, banned from throwing exceptions.
Unlike with move constructors, there are some use cases for destructors that throw exceptions. One such example is a return value that can't be ignored, e.g.,
class ImportantReturn { int value; bool eaten; public: ImportantReturn(int v) : value(v), eaten(false) { } ~ImportantReturn() { if (!eaten) throw important_return_value_ignored(value); } operator int() const { eaten = true; return value; } };
For these destructors (which are a very slim minority) we require a syntax to disable the implicit noexcept
. We propose to use the syntax throw(...)
to mean "this function can throw any exception." This syntax is already a Microsoft extension, and fits in well with existing syntax.
Note that implicitly making destructors noexcept
will break existing code for classes like ImportantReturn
. Users will need to add the throw(...)
specification on these destructors to return them to their C++03 behavior. Given that few destructors need to throw exceptions---and that such classes cannot be used with the C++03 or C++0x standard libraries---we expect that the impact of this breaking change will be small.
noexcept
BlockStatically checking that the body of a function does not throw any exceptions can reject as ill-formed certain programs that are careful to avoid throwing exceptions. As a simple example, imagine a function sqrt(double x)
that computes the square root of x
. If given zero or a negative number, it will throw an std::out_of_range
exception. Now, imagine a function that uses sqrt()
:
double sqrt(double); // may throw exceptions noexcept void f(double &x) { if (x > 0) { x = sqrt(x); // ill-formed: sqrt(x) might throw an exception! } }
This code is ill-formed, because the call to may throw exceptions. The user has properly guarded the call to sqrt
by checking its preconditions within f
, but the compiler is unable to intepret the program to prove that f
will not throw. To address this problem, we propose the addition of a noexcept
block, which will be used as follows:
double sqrt(double); // may throw exceptions noexcept void f(double &x) { if (x > 0) { noexcept { x = sqrt(x); } // okay: if sqrt(x) throws, invokes undefined behavior } }
The noexcept
block states that no exceptions will be thrown by the code within its compound statement. An exception that escapes from a noexcept
block results in undefined behavior.
Exception specifications have not proven to be as useful as we had hoped. Their lack of static checking has limited their usability and confused users, and they provide few benefits for compilers. Moreover, exception specifications aren't useful in generic code, where functions need to know whether an exception can be thrown or not but don't know (or care) what kind of exceptions can be thrown. For these and other reasons, exception specifications are discouraged [1, 2, 3].
In fact, the noexcept
specifier---along with the ability to detect whether an operation is noexcept
via concepts---provides precisely the statically-checked exception specifications that are required in C++ code. We therefore propose to deprecate C++ exception specifications in C++0x: the community has already decided to avoid exception specifications and noexcept
provides a superior alternative.
The introduction of noexcept
and its use throughout the standard library will require significant changes in a few areas. The changes are summarized below.
We propose the introduction of new concepts for non-throwing assignment and construction:
auto concept NothrowConstructible<typename T, typename... Args> : Constructible<T, Args...> { noexcept T::T(Args...); } auto concept NothrowMoveConstructible<typename T> : MoveConstructible<T> { requires RvalueOf<T> && NothrowConstructible<T, RvalueOf<T>::type>; } auto concept HasNothrowAssign<typename T, typename U> : HasAssign<T, U> { noexcept result_type T::operator=(U); } auto concept NothrowMoveAssignable<typename T> : MoveAssignable<T>, HasNothrowAssign<T, T&&> { }
In addition, we need Nothrow
variants of the concepts used for scoped allocators:
concept NothrowConstructibleWithAllocator<class T, class Alloc, class... Args> { noexcept T::T(allocator_arg_t, Alloc, Args&&...); } auto concept NothrowAllocatableElement<class Alloc, class T, class... Args> : AllocatableElement<Alloc, T, Args...> { noexcept void Alloc::construct(T*, Args&&...); }
Finally, we improve the existing NothrowDestructible
concept to statically check the no-throw requirements:
auto concept NothrowDestructible<typename T> : HasDestructor<T> { noexcept T::~T(); }
Throughout the library, each class template that aggregates other templates and has a move constructor will need to have its move constructor and move assignment operator be specified as noexcept
and only be provided when the operations they use are known to be noexcept
. We summarize the changes here, but have omitted the changes for some library components pending a full review of the library.
Two of pair
's move constructors will be modified as follows:
template<class U, class V> requires NothrowConstructible<T1, RvalueOf<U>::type> && NothrowConstructible<T2, RvalueOf<V>::type> noexcept pair(pair<U, V>&& p); template<class U, class V, Allocator Alloc> requires NothrowConstructibleWithAllocator<T1, Alloc, RvalueOf<U>::type> && NothrowConstructibleWithAllocator<T2, Alloc, RvalueOf<V>::type> noexcept pair(allocator_arg_t, const Alloc& a, pair<U, V>&& p);
Similarly, pair
's move assignment operator will be modified as follows:
template<class U , class V> requires HasNothrowAssign<T1, RvalueOf<U>::type> && HasNothrowAssign<T2, RvalueOf<V>::type> noexcept pair& operator=(pair<U , V>&& p);
Two of tuple
's move constructors will be modified as follows:
template <class... UTypes> requires NothrowConstructible<Types, RvalueOf<UTypes>::type>... noexcept tuple(tuple<UTypes...>&&); template <Allocator Alloc, class... UTypes> requires NothrowConstructibleWithAllocator<Types, Alloc, RvalueOf<UTypes>::type>... noexcept tuple(allocator_arg_t, const Alloc& a, tuple<UTypes...>&&);
tuple
's move assignment operator will be modified as follows:
template <class... UTypes> requires HasNothrowAssign<Types, RvalueOf<UTypes>::type>... noexcept tuple& operator=(tuple<UTypes...>&&);
Modify two of deque
's constructors as follows:
requires NothrowAllocatableElement<Alloc, T, T&&> && NothrowMoveConstructible<Alloc> noexcept deque(deque&&); requires NothrowAllocatableElement<Alloc, T, T&&> && NothrowConstructible<Alloc, const Alloc&> noexcept deque(deque&&, const Alloc&);
Modify the deque
move assignment operator as follows:
requires NothrowAllocatableElement<Alloc, T, T&&> && MoveAssignable<T> && NothrowMoveAssignable<Alloc> noexcept deque<T,Alloc>& operator=(deque<T,Alloc>&& x);
Each of the other standard containers and container adaptors will require similar modifications.
noexcept
AnnotationsSince a noexcept
function can only call into other noexcept
functions, much of the standard library itself will require noexcept
annotations to make those parts of the library usable. Therefore, for any function that is currently specified as throw()
we propose to replace the throw()
with a noexcept
specifier. Additionally, for any operation with a clause
we propose to remove this clause and instead introduce the noexcept
specifier on the corresponding declaration.
These changes are likely to effect a significant portion of the Standard Library, including nearly all of the C standard library. In most cases, the changes merely enforce requirements that have already been present in the library.
We have considered several alternative syntaxes for noexcept
, and we catalog the most promising alternatives here:
nothrow
keywordnothrow
would be our first choice for a keyword to describe a non-throwing function (or block). This term has been in use colloquially to describe non-throwing functions and is also used within the standard library. If the technical problems (described below) can be overcome, we would prefer to use nothrow
rather than noexcept
.nothrow
, which is declared as:
extern const nothrow_t nothrow;If
nothrow
were to become a keyword, this object could no longer be defined in the standard library and would have to be removed. We then would have to provide some special syntax allowing existing uses of this object to still work. For example:
using std::nothrow; X* ptr = new (nothrow) X; X* ptr2 = new (std::nothrow) X;
nothrow
attributenothrow
" identifier. In addition, nothrow
is an attribute already supported by the GNU compiler with a similar meaning.GNU
attribute, whose semantics don't exactly match the intended behavior of noexcept
, may prove a hindrance.
!throw
, throw(void)
, throw(not)
)noexcept
behaves very differently from existing exception specifications, so having too many syntactic similarities confuses the semantics of the two features in a way that is likely to cause problems for programmers.