Wednesday, 18 February 2015

C++11 Features - noexcept

Continuing with my last two discussions about exceptions, Catching exception and C++11 Features - Exception: function-try-block, I would like to give exception another section, because it is so importance and troublesome. This tpoic is more or less a summary of Item 14 of Scott Meyer's book - Effective Modern C++, plus some good practice from my experience.

1. Syntax in C++98 and C++11
In C++98 "throw(...)" is the syntax
    - throw(...), for instance throw(std::exception): throws only std::exception plus any derived exception
    - throw(): do not throw
    - function without any notation as "void foo();": function foo() can throw anything or not throwing
As exception specification is part of function declaration. Compilers are vigorously checking the exception specification between the caller function and the callee funtion, the base virtual function and the derived virtual function. As this is so important, becuase
    - Throw an exception in non-throwing function in C++98 can cause program to terminate
    - Throw exceptions that is specified in exception specification can cause program to terminate
    - Non compatible exception specification in base and derived virtual function are actually 2 functions.

All these drawbacks and hassle makes C++98 exception specification rarely practical. And most of function declarations are ended up without any exception function, like void foo(), which can throw anything or not throw at all. Therefore this causes another issue - there is rarely good practice telling other developers if a function is throwing or not by simply reading the header file. I can image how many times exceptions are not caught and cause program to crash during QA.

In C++11, noexcept is introduced to replace throw(...).
As a recognition that the C++98 exception specification is not useful and practical, C++11 introduced noexcept to express a function that is either throwing or not at all. There is no "specific exception" that can be passed to throw(). In C++11 there are only two choices - either throw or not throw at all. Syntax follows,
    - void foo() noexcept;
    - void bar() noexcept(false);
    - by default noexcept is noexcept(true)
    - noexcept can take bool expression that can be evaluated at the compilation time. See the examples from Effective Modern C++.
    - function without exception specification has the same meaning as C++98, either throw anything or not throw at all.

As C++11 still upholds that exception specification is part of function declarations, The exception compatibility between caller and callee, and between base and derived virtual function are still being checked by compilers. But in C++11 exception specification has only two choices, hence there are only 4 combinations after cross-over. Compatibility is allowed that narrowing from callee to caller, from derived to base virtual.
    - Narrowing from noexcept(false) to noexcept(true) is not allowed
    - The rest combination is allowed

By the way C++98 syntax is still allowed in C++11 for back-compatibility.
 
2. Optimization
As Scott Meyer stated in his book that C++98 guarantees that in the no-throw exception sepcification the callee's stack is unwound to its caller's function and the program terminates when an exception specification is violated, for instance an exception leaves out a function with no throw exception specification. However C++11 does not guarantee the stack unwinding will definitely happen - it "possibly" happens. This means that C+98 generates code to guarantee that the runtime stack is in a unwindable state and make sure the objects in stack are destroyed in the reverse order. However C++11 does  not guarantee that. This means that C++11 does not need to generate the code to guarantee stack-unwinding when using noexcept/noexcept(true) and therefore possibly generate less and more efficient code.

Another potential performance boost is coming from move semantics. For instance used in standard template container and some standard algorithm like std::swap. Move semantics requires noexcept on object's destructor or some of its constructors.

3. Destructor
In C++98 it is regarded as common/good practice not to throw exceptions in destructor, because encountering another exception during in stack unwinding caused by an exception terminates the program. The destructor is where the objects release themselves and return resource back to system. All member variables' destructors are called in the reverse order of their construction. If any two of them throwing exceptions and allowing to leave their destructor, then the disaster described above will happen - the program will be brought down.

In C++11 this common practice has been brought up to a language rule - any memory deallocation and destructor are by default implicitly declared as "noexcept" no matter it is user-defined or generated by compiler. When all member variables have noexcept destructor, this class has noexcept destructor implicitly. If not all member variables have noexcept destrucotr, then this class does not have noexcept destructor either implicitly, because the exception specification can't narrow from noexcept(false) to noexcept(true). And the behavior is undefined if objects with noexcept(false) destructor are used with standard template container and standard algorithms.

At the meantime C++11 allows to specify destructor throwing exceptions with exception specification as "noexcept(false)", just as C++98 allows destructor to throw. (Never did this. One occasion reminded of this, when I was developing database related application. The warpper class of DB connection - throw exception if can't terminate the connection with the DB server in the destructor. But soon I abandoned this idea to use a Close() member function to terminate the DB connection and throw exception/oddity to system to notify the program and then have a non-throwing destructor.)

4. STL
A lot of C++11 features are introduced to improve the performance. And a lot of them are actually targeting at improving the performance of STL. One of key issues is that a lot of algorithms or functions of STL involves object copying/moving. Features like move semantics, rvalue-reference are introduced to solve this issue. With these features STL will employ move semantics rather than copy semantics if possible. When STL coming to decide use copy/move, one of principle is to check if this object's destructor and some of its constructors are exception free. More details please refer to Chapter 5 of Effective Modern C++.

5. Practical Use
Use noexcept where you can but do not overuse it. Just as Scott Meyer suggested in this book, declaring a API function as noexcept is a strong commitment. Declare them as noexcept if you'are absolutely sure and committed.

Rarely declare destructor as noexcept(false), because it could crash the problem. And keep in mind that do not use these objects with STL or algorithms.

One of good examples is to declare the functions of POD class (refer to Plain Old Data (POD) on C++11) as noexcept as many as possible. And define move operations as well if POD objects are used with STL. Engage with the potential performance boost

Use noexcept(...) with the functions that call the API functions that has exception specification. Keep the exception specification compatible and notify the users that this function may throw.

6. Thing Remaining Unsolved
In C++98 one of  issues that I am struggling most is how to write functions declaration to tell if this function is throwing exceptions.As we discussed above the exception specification is rarely practical in C++98. In last a few years I discussed with my colleagues many many occasions how to clearly express if a function is throwing. But not a time we can reach an agreement. We tried
    - Use documentation/comments: drawback is maintainability
    - Use different naming convention (with suffix like _throw) for the function throwing: drawback is that have to add suffix for on the functions of the entire calling graph, which just needs too much effort.

This issue remains in C++11, because functiosn without any exception specification can either throw or not throw. This uncertainty will definitely catch us and the program will crash because of uncaught exceptions. Mistakes can be easily made especially when there is no agreement to function declaration to distinguish functions throwing or not-throwing, In my personal experience I was caught quite a few times. Especially in a large code base or application involving the 3rd-party software there is more chances to be caught.

Google C++ Coding Style recommends not using exception at all. One of the reasons is mainly because of its legacy code. I don't agree with this. What I am suggesting follows,
    - Agree some rules to function declaration to tell throwing and non-throwing functions
    - Use exceptions only for user-action related error
    - Use error code for the rest
    - All user-defined exceptions derived from std::exception

Bibliography:
[1] Scotts Meyer, "Effective Mordern C++", O'REILLY, 2014 First Release
[2] Bjarne Stroustrup, "The C++ Programming Language", C++11, 4th Edition
[3] C++ Reading Club @ IG Group

No comments:

Post a Comment