In C++11, a natural optimization would be to replace the copying of std::vector
elements with moves. Unfortunately, doing this runs the risk of violating push_back's exception safety guarantee. If n
elements have been moved from the old memory and an exception is thrown moving element n+1
, the push_back
operation can't run to completion. But the original std::vector
has been modified: n
of its elements have been moved from. Restoring their original state may not be possible, because attempting to move each object back into the original memory may itself yield an exception.
This is a serious problem, because the behavior of legacy code could depend on push_back
's strong exception safety guarantee. Therefore, C++11 implementations can't silently replace copy operations inside push_back
with moves unless it's known that the move operations won't emit exceptions. In that case, having moves replace copies would be safe, and the only side effect would be improved performance.
std::vector::push_back
takes advantage of this “move if you can, but copy if you must” strategy, and it's not the only function in the Standard Library that does. Other functions sporting the strong exception safety guarantee in C++98 (e.g., std::vector::reserve
, std::deque::insert
, etc.) behave the same way. All these functions replace calls to copy operations in C++98 with calls to move operations in C++11 only if the move operations are known to not emit exceptions. But how can a function know if a move operation won't produce an exception? The answer is obvious: it checks to see if the operation is declared noexcept
. [3] The checking is typically rather roundabout. Functions like std::vector::push_back call std::move_if_noexcept , a variation of std::move that conditionally casts to an rvalue (see Item 23 ), depending on whether the type's move constructor is noexcept . In turn, std::move_if_noexcept consults std::is_nothrow_move_constructible , and the value of this type trait (see Item 9 ) is set by compilers, based on whether the move constructor has a noexcept (or throw() ) designation.
swap functions comprise another case where noexcept
is particularly desirable. swap
is a key component of many STL algorithm implementations, and it's commonly employed in copy assignment operators, too. Its widespread use renders the optimizations that noexcept
affords especially worthwhile. Interestingly, whether swap
s in the Standard Library are noexcept
is sometimes dependent on whether user-defined swap
s are noexcept
. For example, the declarations for the Standard Library's swap
s for arrays and std::pair
are:
template
void swap(T (&a)[N], // see
T (&b)[N]) noexcept(noexcept(swap(*a, *b))); // below
template
struct pair {
…
void swap(pair& p) noexcept(noexcept(swap(first, p.first)) &&
noexcept(swap(second, p.second)));
…
};
These functions are conditionally noexcept
: whether they are noexcept
depends on whether the expressions inside the noexcept
clauses are noexcept
. Given two arrays of Widget
, for example, swap
ping them is noexcept
only if swap
ping individual elements in the arrays is noexcept
, i.e., if swap
for Widget
is noexcept
. The author of Widget
's swap
thus determines whether swap
ping arrays of Widget
is noexcept
. That, in turn, determines whether other swap
s, such as the one for arrays of arrays of Widget
, are noexcept
. Similarly, whether swap
ping two std::pair
objects containing Widget
s is noexcept
depends on whether swap
for Widget
s is noexcept
. The fact that swapping higher-level data structures can generally be noexcept
only if swapping their lower-level constituents is noexcept
should motivate you to offer noexcept swap
functions whenever you can.
By now, I hope you're excited about the optimization opportunities that noexcept
affords. Alas, I must temper your enthusiasm. Optimization is important, but correctness is more important. I noted at the beginning of this Item that noexcept
is part of a function's interface, so you should declare a function noexcept
only if you are willing to commit to a noexcept
implementation over the long term. If you declare a function noexcept
and later regret that decision, your options are bleak. You can remove noexcept
from the function's declaration (i.e., change its interface), thus running the risk of breaking client code. You can change the implementation such that an exception could escape, yet keep the original (now incorrect) exception specification. If you do that, your program will be terminated if an exception tries to leave the function. Or you can resign yourself to your existing implementation, abandoning whatever kindled your desire to change the implementation in the first place. None of these options is appealing.
The fact of the matter is that most functions are exception-neutral . Such functions throw no exceptions themselves, but functions they call might emit one. When that happens, the exception-neutral function allows the emitted exception to pass through on its way to a handler further up the call chain. Exception-neutral functions are never noexcept
, because they may emit such “just passing through” exceptions. Most functions, therefore, quite properly lack the noexcept
designation.
Some functions, however, have natural implementations that emit no exceptions, and for a few more — notably the move operations and swap
— being noexcept
can have such a significant payoff, it's worth implementing them in a noexcept
manner if at all possible. [4] The interface specifications for move operations on containers in the Standard Library lack noexcept. However, implementers are permitted to strengthen exception specifications for Standard Library functions, and, in practice, it is common for at least some container move operations to be declared noexcept. That practice exemplifies this Item's advice. Having found that it's possible to write container move operations such that exceptions aren't thrown, implementers often declare the operations noexcept, even though the Standard does not require them to do so.
When you can honestly say that a function should never emit exceptions, you should definitely declare it noexcept
.
Please note that I said some functions have natural noexcept
implementations. Twisting a function's implementation to permit a noexcept
declaration is the tail wagging the dog. Is putting the cart before the horse. Is not seeing the forest for the trees. Is…choose your favorite metaphor. If a straightforward function implementation might yield exceptions (e.g., by invoking a function that might throw), the hoops you'll jump through to hide that from callers (e.g., catching all exceptions and replacing them with status codes or special return values) will not only complicate your function's implementation, it will typically complicate code at call sites, too. For example, callers may have to check for status codes or special return values. The runtime cost of those complications (e.g., extra branches, larger functions that put more pressure on instruction caches, etc.) could exceed any speedup you'd hope to achieve via noexcept
, plus you'd be saddled with source code that's more difficult to comprehend and maintain. That'd be poor software engineering.
Читать дальше