The Debate on noexcept, Part II
The Debate on noexcept, Part III
The Debate on noexcept, Part I
Last updated Apr 7, 2010.
At the Pittsburgh meeting in March 2010, the ISO C++ committee added the new keywordnoexcept to the FCD. The precise semantics of noexcept is still a subject for debates. Here I will outline the reasons for adding noexcept, contrast it with traditional exceptions specifications that were just deprecated, and introduce the main points of controversy.
Exception Specifications
There's been a long standing consensus that the C++98 exception specifications are no good. There are several problems with them, most of which have been discussed in length previously. I'll briefly summarize them again.
Dynamic checking. Exception specifications are checked at runtime, not at compile-time. Consequently, programmers calling a function with a certain exception specification can't be sure that the exception specification will be honored at runtime. Furthermore, an implementation must generate additional code to perform the runtime checking of exceptions. That leads to inefficient code and hampers certain optimizations.
Generic Code. It's difficult to write accurate exception specifications in generic code because the designer can't tell what types of exceptions may be thrown from operations on template arguments.
The vast experience of the last two decades shows that in practice, only two forms of exceptions specifications are useful:
- The lack of an overt exception specification, which designates a function that can throw any type of exception:
int func(); //might throw any exception
int add(int, int) nothrow; //never throws
In practice, most functions that never throw don't include an overt throw() specification because exception specifications have fallen from grace, and because a throw() specification incurs performance overhead (I'll discuss the overhead shortly).
These facts are known to the majority of C++ programmers. It's no wonder then that the committee decided in its last meeting to deprecate exception specifications.
Note: The traditional exception specifications in the form
int f() throw(x,y...);
are now referred to as "dynamic exception specifications", to make them distinct from the newly added keyword noexcept, which serves as a compile-time exception specification.
Why Not Stop At Deprecation?
If the committee had settled for deprecating dynamic exception specifications, very few eyebrows would have been raised. However, some members felt that a mechanism for indicating a function that never throws was necessary (especially in move operations) as an optimization cue to the compiler. At this point you're probably wondering: "isn't that what the throw() exception specification does anyway?" Yes, but not exactly. Indeed throw() indicates that a function doesn't throw. However, suppose that a function declared throw() violates its exception specification and throws an exception:
void f() (const char * text) throw();
{
std::string s(text);
}
Although f() doesn't have an overt throw statement, its throw() specification is misleading because the constructor of std::string might throw. What should happen in that case? The C++98 standard requires that the implementation shall unwind the stack of the offending function (invoke destructors of local objects, flush I/O streams etc) and then invoke the Standard Library function std::unexpected(). unexpected() may try (at least in theory) to recover from the exception, which is why an implementation must ensure proper unwinding of the stack. That "proper unwinding of the stack" is what inhibits optimizations of functions declared throw().
In contrast, noexcept implies no stack unwinding. Furthermore, whereas a dynamic exception specification is checked at runtime, the noexcept keyword is checked at compile-time, when possible. Although the compiler can't detect every violation of noexcept, it will issue a diagnostic if it can determine that a function declared noexcept might throw, as in the following example:
void g() noexcept
{
throw Myexception("disaster!"); //C++0x compilation error/warning
}
I believe that at this stage of the C++0x standardization process, the best approach would have been to stop at the deprecation of dynamic exception specifications. We know they're bad and we don't use them anyway. It's too late now to invent a new exception specification mechanism just days before the approval of the FCD. However, that's exactly what happened in Pittsburgh -- the new keyword noexcept was added to the FCD, causing controversy (the minutes from that meeting are available here).
How Does noexcept Differ from throw()?
The proposal doesn't provide all the details with respect to the semantic differences betweenthrow() and noexcept. Generally speaking, the latter is (supposedly) a compile-time specification, as opposed to the former which is checked at runtime. Practically, that means two things:
A compiler may be able to apply certain optimizations to a function declared noexcept. Those optimizations are disabled by default because a function that's not declared noexcept might throw any exception type.
Additionally, the proposal requires that in the event of a noexcept violation, the implementation shall call terminate() (instead of unexpected()) More specifically, the implementation shall notinvoke local objects' destructors if a noexcept violation has occurred.
The Debate on terminate()
The requirement to invoke terminate() in the event of a noexcept violation started a debate: Why not state that noexcept violations causes undefined behavior? After all, a noexcept violation shouldn't occur, and if it does, it shouldn't be different from other instances of bad programming practice, say deleting the same pointer twice, buffer overflows etc. Recall that undefined behaviormeans that anything can happen, including what appears to be normal execution flow.
Another practical argument against calling terminate() was raised. A number of domain areas, such as embedded programming, require terminate() to never be called, insisting that an application should continue to run forever. Therefore, it may be best to let each implementation decide how to handle noexcept violations instead of prescribing that terminate() shall be called.
However, other members strongly opposed to the idea of allowing a program to continue executing past the noexcept call because the results of continuing the program's execution without unwinding the stack might be disastrous -- leaked locks, corrupted files etc. Furthermore, programmers writing code for a catch clause would not be able to distinguish a "valid exception" from an exception that has violated noexcept and therefore couldn't tell how to handle an exception at all.
In the next part of this series I will discuss the arguments in favor of, and against invokingterminate() in depth, and try to predict what will come out of this proposal.
======================================
The Debate on noexcept, Part II
Last updated Apr 16, 2010.
In the previous part I outlined the limitations of dynamic exception specifications and discussed the adoption of the new C++0x noexcept keyword into the FCD. This part will dig deeper into the technical aspects of noexcept and the controversy surrounding it.
Why noexcept is Needed
The proponents of noexcept claim that marking certain functions as noexcept may improve performance -- especially in Standard Library algorithms and containers, as opposed to usingthrow(). The deprecated throw() exception specification forces the compiler to generate auxiliary code to intercept runtime violations of a function's exception specification. That auxiliary code is needed for unwinding the stack, among the rest. This property of dynamic exception specifications is what made them rather useless. If the implementation must always assume that an exception might be thrown -- even from a function declared throw() -- why bother with exception specifications in the first place? As opposed to throw(), noexcept allows the compiler to forego the generation of that auxiliary code. As a result, the compiler can generate code that's more efficient -- both the code of the function flagged as noexcept, and the code that calls such a function.
Potential Complexities
Troubles start when the noexcept guarantee is violated. Roughly speaking, we can divide noexceptviolations into two major categories:
Violations that the compiler can detect. According to the proposal, the compiler should issue a diagnostic (either a warning or an error) when it can tell that a function declared noexcept will violate this guarantee, as in:
int func() noexcept
{
throw SomeException("failure");
}
func() is ill-formed. The compiler can see that func() always throws an exception, thus violating its noexcept guarantee. A more realistic example of noexcept violations requires more subtle code analysis:
int f() noexcept
{
std::string s("test");
}
Should f() pass compilation? A rigorous exception safety model requires that the compiler shall issue at least a warning because the constructor of std::string might throw. So, what should the compiler do in this case? The proposal doesn't prescribe a normative policy. However, the tendency is to accept this code, following the "trust the programmer" maxim.
What should the implementation do if f() throws an exception? According to the proposal,std::terminate() shall be called, without unwinding the stack. Others prefer to leave the policy of handling noexcept violations implementation-defined or even undefined. The FCD requires thatstd::terminate() shall be called.
Violations that the compiler cannot detect. Suppose you have a function f1() that calls another function f2() defined in a different translation unit. f1() is declared noexcept, whereas f2() isn't. When the compiler processes the definition of f1() it cannot be certain that f1() will not throw because it has no access to the definition of f2(). Again, the current proposal doesn't prescribe a universal policy for such cases. However, it appears that the compiler should respect the noexceptguarantee of f1() -- so long as there's no positive evidence flouting this guarantee. Consider a source file that looks like this:
void f2();
void f1() noexcept
{
f2();
}
The compiler will accept the code without a hitch. However, in the following example the compiler is expected to complain:
void f2()
{
throw "error!";
}
void f1() noexcept //should cause compilation error
{
f2(); //this function throws
}
This partial compile-time checking policy raises two more issues.
Compilation Complexity and Possible Redundancy
The first issue is compilation complexity. If the compiler is expected to perform source code analysis that "sees through" function calls, build-time will increase considerably.
You may have noticed something strange here -- noexcept is seemingly redundant. If the compiler checks whether the noexcept guarantee isn't violated, why not leave this task entirely to the compiler? The compiler knows (at least in some cases) which functions might throw and which ones will never throw. Why force the programmer to declare functions as noexcept? This argument may sound familiar to you: that's exactly my argument against using inline. Both keywords suggest an optimization strategy that the compiler is free to reject. So, does the compiler neednoexcept at all?
Theoretically, we can let the compiler express the notion of a noexcept function with some name-mangling magic. This will ensure (with a little help from the linker) that calling a function that's been flagged as noexcept will not incur the overhead of exception handling. However, no one has proposed such a model because it will break the current C++ ABI. Although the C++ABI is platform-dependent, it's a relatively stable feature. Programmers get upset when it changes because they have to recompile and relink every piece of code, including legacy shared libraries. Besides, such a model might make C++ compilation and linkage unduly complex and time-consuming.
In the last part of this series I will show why noexcept isn't truly redundant, and conclude whether it's a useful feature or one that should be scrapped.
=====================
The Debate on noexcept, Part III
Last updated Apr 23, 2010.
In the final part of this series I will summarize the pros and cons and conclude the discussion about the fate of noexcept and its usefulness.
Redundant? Not Quite
In the previous part I raised the "redundancy claim", which essentially suggests that noexceptmight be redundant because in some cases at least, the compiler can check a function's code to determine whether it will throw an exception. If the compiler can prove that a function will not throw, it doesn't need noexcept in the first place.
The proponents of noexcept raise an important counter-argument. Indeed, when the compiler processes a function definition it can sometimes tell whether that function will not throw without anoexcept declaration. However, this is true only for functions whose definitions are visible to the compiler. If a certain translation unit contains a call to a function defined in a different translation unit, the compiler has no way of knowing whether that function throws. That's where the noexceptkeyword proves to be most useful -- it allows both the programmer and the compiler to tell that a certain function doesn't throw. Looking back at our previous example:
void f2() noexecpt;
void f1() noexcept
{
f2(); //OK, calling a noexcept function
}
Adding noexcept to the prototype of f2() seemingly resolves the dilemma -- the compiler assumes that the call to f2() will not throw, which means that f1() will not throw either. But what if the programmer who wrote the declaration of f2() with noexcept was wrong? What if the definition off2() changes in the future, turning f2() into a function that does throw? Will the programmer remember to update all the declarations of f2()? Unlike inline, which can't cause too much harm even if it's wrong, noexcept isn't to be taken lightly -- a noexcept violation can be disastrous. That is why I believe that noexcept is bad idea to begin with. Indeed, it may improve performance in some cases but the risks are too high. The proposal also assumes that the programmer is an exception safety expert, which is rarely the case.
Summarizing the Problems with noexcept
Let's summarize the problems with the current specification of noexcept.
No existing practice. The committee fell again into the well-known trap of "design by committee", standardizing a feature that had never been implemented in the real world. If noexcept had been proposed a couple of years ago, at least we would have had some time to evaluate it, fine-tune it or even scrap it. Alas, the noexcept proposal appeared out of the blue, just days before the FCD's closing date.
Underspecification. The proposal boasts "compile-time checking" but that's false advertising. In some cases, the compiler is supposed to validate the noexcept guarantee. However, there are cases in which the compiler can't do that. Worse yet, the proposal doesn't state which tests the compiler should perform and what's considered a "show-stopper". For example, is it OK to declare a function that might rarely throw noexcept? Should the compiler dig deep into every possible execution path of a noexcept function?
Benchmarking. Thus far, no official benchmark results have been published. It's therefore not clear what the potential performance gain of noexcept is. What if it turns out to be negligible? Will that justify the risks associated with noexcept? Even if the performance improvement is staggering, the results should have been published before the Pittsburgh meeting, not after.
Disagreement about noexcept violations. Although the proposal requires that std::terminate()shall be called if a noexcept function throws, that policy is both controversial and underspecified. Which resources are supposed to be released in such case? Should the program crash unconditionally, causing resource leaks and risking data integrity? I can understand why stack unwinding is ruled out but what about other resources such as locks and system I/O buffers? Letting an airplane crash just because a sloppy programmer forgot to remove noexcept from an old header file doesn't sound reassuring.
Compile-time checking is unrealistic. Let's admit it: unless the C++ standard enforces Whole Program Analysis, it's simply ridiculous to assume that the compiler will apply compile-time checking of noexcept. If the compiler can do that only in certain cases, that's the worst option because programmers will never know whether a successful compilation means that a noexceptfunction has been validated by the compiler or does that mean that the compiler was unable to validate the noexcept guarantee and therefore trusted the programmer's judgment. We have plenty of experience with register and inline suggesting that programmers' judgment -- as far as optimization is concerned -- doesn't necessarily outwit the compiler's.
In Conclusion
Although noexcept is in the FCD, we still don't have full specifications thereof nor do we have an exhaustive checklist of what's required of the implementation. I believe that as with other questionable C++0x features, noexcept is a recipe for trouble, and that its benefits are offset by its drawbacks. Premature optimization is evil, as we all know too well.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.