# `double static noexcept` -- a mechanism for static checking of noexcept blocks ## Table of Contents 1. Abstract 2. Discussion 3. Proposal 3. Examples ## Abstract In C++, `noexcept` exists to indicate that a function does not throw an exception. However, what it actually provides is that a function cannot possibly throw an exception. This is enforced by "violence", if necessary -- the program will terminate rather than allow an exception to pass a `noexcept` boundary. Unfortunately, this turns `noexcept` into a ticking timebomb; any `noexcept( true )` function may unexpectedly call `std::terminate` at any time, thus aborting a program. This may be undesirable in many different contexts, from Functional Safety to Program Correctness to Contractual Guarantees. This proposal provides a mechanism which permits various levels of static guarantees. ## Discussion The `noexcept` facility was added to permit a language level (and type-system level) facility for promising that a function does not throw and for checking it. However, the enforcement of this promise is a purely runtime concern. This leads to troublesome timebombs, such as this code. ~~~ void silentlyThrowing() noexcept( false ) { throw 0; } void tickingTimeBomb() noexcept( true ) { if( not ( rand() % 32 ) ) { silentlyThrowing(); } } enum Result { Success, Failure }; Result cannotTolerateExceptions() noexcept( true ) { std::string element; try { element= someVector.at( 42 ); if( someVector.capacity() == someVector.size() ) { someVector.reserve( someVector.size() * 2 ); } } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } someVector.push_back( std::move( element ) ); tickingTimeBomb(); return Success; } ~~~ While some of the code and functions called in `cannotTolerateExceptions` are capable of throwing, the caller attempts to resolve exceptions and handle them without program termination. However, the `tickingTimeBomb` function is a hidden hazard. While it declares itself to not throw exceptions (which is true), the exceptions thrown by code it calls are not checked and will "bump into" the `noexcept( true )` on `tickingTimeBomb`. As such, the program will terminate, despite the best efforts of the `cannotTolerateExceptions()` implementation. The language requires a mechanism to statically check whether callees are equally "responsible" about termination semantics as the caller. To this end, we propose three new mechanisms. ## Proposal We propose a few "extensions" to the `noexcept` keyword used on function declarators. We will outline these here. ##### Note about keyword bikeshedding This paper proposes "compound" keywords by recycling the meaning of existing keywords. The actual keyword or keyword sequences to define these kinds of `noexcept` function are immaterial to the mechanics of this proposal and thus we defer the decision of keywords at this time. ----------------------- ### First new feature A statically checked `noexcept` function - `static noexcept( true )` We propose the introduction of `static noexcept` as both a function declaration "decorator" and an operator. `static noexcept( true )` or `static noexcept` tagged functions have similar semantics to `noexcept` tagged or colored functions: 1. An evaluation of `noexcept( someStaticNoexceptFunction )` will evaluate as `true`. 2. An evaluation of `static noexcept( someStaticNoexceptFunction )` will evaluate as `true`. 3. An evaluation of `static noexcept( someClassicNoexceptFunction )` will evaluate as `false`. 4. A call to a `static noexcept( true )` function will not throw. 5. If an exception unwind (somehow) attempts to "emerge" from a `static noexcept( true )` function, then the program will terminate. However, we propose to add the following behaviors and restrictions to such functions: 1. If a `noexcept( false )` function is called outside of the body of a `try` block, then the program is ill formed, and a compile-time diagnostic is required. 2. If a `noexcept( false )` function is called inside the body of a `try` block, then a `catch( ... )` block must exist. 3. A `catch` block's body is not considered part of its `try` block. 4. When checking if a statement is "in" a `try` block, one must walk up the nested block structure until a try block is reached. A `catch` block attached to a `try` block which is inside of a try block at broader scope is considered to be "inside" the broader try block, for the purpose of these rules. 5. A simple model of this is that invoking `noexcept( false )` functions is ill formed unless a `try` block scope can be found by walking outward from the calling scope. 6. It is unclear at this time if language UB on expressions should be considered throwing or not. There are tradeoffs. 1. If UB is considered `noexcept( true )`, then there is a potential that UB can cause program termination by "leaking" an exception out from checked try blocks. 2. If UB is considered `noexcept( false )`, then it makes writing `static noexcept( true )` extremely difficult. Many situations will require noisy and defensive `try { /* code */ } catch( ... ) { /* silently ignore exception and do nothing */ }` patterns. The authors of this paper favor the interpretation that despite UB, most language primitive behavior should be considered `noexcept( true )` for the purpose of this checking. There already exists the possibility of "deeper down" time bombs in this construct, and thus language UB leading to exceptions is a somewhat niche case. #### Rewriting the original painful example using this feature ~~~ void silentlyThrowing() noexcept( false ) { throw 0; } void tickingTimeBomb() noexcept( true ) { if( not ( rand() % 32 ) ) { silentlyThrowing(); } } enum Result { Success, Failure }; Result illFormedCannotTolerateExceptions() static noexcept( true ) { std::string element; try { element= someVector.at( 42 ); if( someVector.capacity() == someVector.size() ) { someVector.reserve( someVector.size() * 2 ); } } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } someVector.push_back( std::move( element ) ); // This line would be ill formed tickingTimeBomb(); return Success; } Result cannotTolerateExceptions() static noexcept( true ) { try { auto element= someVector.at( 42 ); // This line has to be moved into the try block to satisfy the `static noexcept` // requirements. // // As such the "somewhat defensive against exceptions" reserve can be eliminated. someVector.push_back( std::move( element ) ); } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } tickingTimeBomb(); return Success; } ~~~ ----------------------- ### Attempt at a second new feature: A stronger (strawman) statically checked function - `false static noexcept( true )` Because the troublesome `tickingTimeBomb` function, which was marked `noexcept` still could be called, it's clear that `static noexcept` is insufficiently "sharp" as to catch all time bombs. Yet is still has utility as a tool to help carefully construct functions which are more resilient to unexpected termination. That form (`static noexcept`) provides a comfortable middle ground for many cases. This will become apparent when we introduce our stronger checking mechanisms, `false static noexcept` and `double static noexcept`. It should be noted that `false static noexcept` is a "straw man". It appears to be the correct solution, but it still has traps. We present this one before our real solution, `double static noexcept`. This solution helps expose and explore the problem space. For the `false static noexcept` operator, we propose the following: 1. An evaluation of `noexcept( someFalseStaticNoexceptFunction )` will evaluate as `true`. 2. An evaluation of `static noexcept( someFalseStaticNoexceptFunction )` will evaluate as `true`. 3. An evaluation of `false static noexcept( someClassicNoexceptFunction )` will evaluate as `false`. 4. An evaluation of `false static noexcept( someStaticNoexceptFunction )` will evaluate as `false`. 5. An evaluation of `false static noexcept( someFalseStaticNoexceptFunction )` will evaluate as `true`. 6. A call to a `false static noexcept( true )` function will not throw. 7. If an exception unwind (somehow) attempts to "emerge" from a `false static noexcept( true )` function, then the program will terminate. However, we propose to add the following behaviors and restrictions to such functions: 1. If a `false static noexcept( false )` function is called outside of the body of a `try` block, then the program is ill formed, and a compile-time diagnostic is required. 2. If a `false static noexcept( false )` function is called inside the body of a `try` block, then a `catch( ... )` block must exist. 3. A `catch` block's body is not considered part of its `try` block. 4. When checking if a statement is "in" a `try` block, one must walk up the nested block structure until a try block is reached. A `catch` block attached to a `try` block which is inside of a try block at broader scope is considered to be "inside" the broader try block, for the purpose of these rules. 5. A simple model of this is that invoking `false static noexcept( false )` functions is ill formed unless a `try` block scope can be found by walking outward from the calling scope. 6. It is unclear at this time if language UB on expressions should be considered throwing or not. There are tradeoffs. 1. If UB is considered `noexcept( true )`, then there is a potential that UB can cause program termination by "leaking" an exception out from checked try blocks. 2. If UB is considered `noexcept( false )`, then it makes writing `static noexcept( true )` extremely difficult. Many situations will require noisy and defensive `try { /* code */ } catch( ... ) { /* silently ignore exception and do nothing */ }` patterns. The conclusion about whether language UB should be considered `false static noexcept( true )` for the purpose of this checking is irrelevant, as this form exists merely for expository purposes. #### Rewriting the original painful example using this feature ~~~ void silentlyThrowing() noexcept( false ) { throw 0; } void wrappedSilentlyThrowing() noexcept( true ) { silentlyThrowing(); } // We try to be more responsible void tickingTimeBomb() false static noexcept( true ) try { if( not ( rand() % 32 ) ) { silentlyThrowing(); // This gets handled in the catch below. } if( not ( rand() % 32 ) ) { // The rules for `false static noexcept` permit this function to be called, // despite it being a program termination hazard. wrappedSilentlyThrowing(); } } catch( ... ) { // Handle unexpected exceptions. } enum Result { Success, Failure }; Result cannotTolerateExceptions() false static noexcept( true ) { try { auto element= someVector.at( 42 ); someVector.push_back( std::move( element ) ); } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } // Despite the `false static noexcept` rules claiming that this is safe, // it is still a ticking timebomb in its own right, and thus for this function, too. // // The key take-away here is that despite `false static noexcept`'s recursive enforcement // rule, we still have termination leaks. tickingTimeBomb(); return Success; } ~~~ ----------------------- ### A second new feature: A static no-termination guarantee -- `do not break if using this` All of the major troubles we find with recursive exception safety guarantees bump into a limitation. It is currently not possible in C++ to know whether a function has a termination hazard. We thus propose a `do not break if using this` operator and function decorator. #### A note on name choice While `noterminate` is probably the most obvious name here, there are some concerns with it. Particularly that `noterminate` implies that it would never terminate or call `std::terminate`. #### The `do not break if using this` specification We propose the introduction of `static noexcept` as both a function declaration "decorator" and an operator. `static noexcept( true )` or `static noexcept` tagged functions have similar semantics to `noexcept` tagged or colored functions: 1. An evaluation of `noexcept( someStaticNoexceptFunction )` will evaluate as `true`. 2. An evaluation of `static noexcept( someStaticNoexceptFunction )` will evaluate as `true`. 3. An evaluation of `static noexcept( someClassicNoexceptFunction )` will evaluate as `false`. 4. A call to a `static noexcept( true )` function will not throw. 5. If an exception unwind (somehow) attempts to "emerge" from a `static noexcept( true )` function, then the program will terminate. =-=-=-=-=-=-=-=-=-=- # `double static noexcept` -- a mechanism for static checking of noexcept blocks ## Table of Contents 1. Abstract 2. Discussion 3. Proposal 3. Examples ## Abstract In C++, `noexcept` exists to indicate that a function does not throw an exception. However, what it actually provides is that a function cannot possibly throw an exception. This is enforced by "violence", if necessary -- the program will terminate rather than allow an exception to pass a `noexcept` boundary. Unfortunately, this turns `noexcept` into a ticking timebomb; any `noexcept( true )` function may unexpectedly call `std::terminate` at any time, thus aborting a program. This may be undesirable in many different contexts, from Functional Safety to Program Correctness to Contractual Guarantees. This proposal provides a mechanism which permits various levels of static guarantees. ## Discussion The `noexcept` facility was added to permit a language level (and type-system level) facility for promising that a function does not throw and for checking it. However, the enforcement of this promise is a purely runtime concern. This leads to troublesome timebombs, such as this code. ~~~ void silentlyThrowing() noexcept( false ) { throw 0; } void tickingTimeBomb() noexcept( true ) { if( not ( rand() % 32 ) ) { silentlyThrowing(); } } enum Result { Success, Failure }; Result cannotTolerateExceptions() noexcept( true ) { std::string element; try { element= someVector.at( 42 ); } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } someVector.push_back( std::move( element ) ); tickingTimeBomb(); return Success; } ~~~ While some of the code and functions called in `cannotTolerateExceptions` is capable of throwing, the caller attempts to resolve exceptions and handle them without program termination. However, the `tickingTimeBomb` function is a hidden hazard. While it declares itself to not throw exceptions (which is true), the exceptions thrown by code it calls are not checked and will "bump into" the `noexcept( true )` on `tickingTimeBomb`. As such, the program will terminate, despite the best efforts of the `cannotTolerateExceptions()` implementation. The language requires a mechanism to statically check whether callees are equally "responsible" about termination semantics as the caller. To this end, we propose three new mechanisms. ## Proposal We propose a few "extensions" to the `noexcept` keyword used on function declarators. We will outline these here. ##### Note about keyword bikeshedding This paper proposes "compound" keywords by recycling the meaning of existing keywords. The actual keyword or keyword sequences to define these kinds of `noexcept` function are immaterial to the mechanics of this proposal and thus we defer the decision of keywords at this time. ### A statically checked `noexcept` function - `static noexcept( true )` We propose the introduction of `static noexcept` as both a function declaration "decorator" and an operator. `static noexcept( true )` or `static noexcept` tagged functions have similar semantics to `noexcept` tagged or colored functions: - An evaluation of `noexcept( someStaticNoexceptFunction )` will evaluate as `true`. - An evaluation of `static noexcept( someStaticNoexceptFunction )` will evaluate as `true`. - An evaluation of `static noexcept( someClassicNoexceptFunction )` will evaluate as `false`. - A call to a `static noexcept( true )` function will not throw. - If an exception unwind (somehow) attempts to "emerge" from a `static noexcept( true )` function, then the program will terminate. However, we propose to add the following behaviors and restrictions to such functions: - If a `noexcept( false )` function is called outside of the body of a `try` block, then the program is ill formed, and a compile-time diagnostic is required. - If a `noexcept( false )` function is called inside the body of a `try` block, then a `catch( ... )` block must exist. - A `catch` block's body is not considered part of a `try` block. - When checking if a statement is "in" a `try` block, one must walk up the nested block structure until a try block is reached. A `catch` block attached to a `try` block which is inside of a try block at broader scope is considered to be "inside" the broader try block, for the purpose of these rules. - A simple model of this is that invoking `noexcept( false )` functions is ill formed unless a `try` block scope can be found by walking outward from the calling scope. - It is unclear at this time if language UB on expressions should be considered throwing or not. There are tradeoffs. - If UB is considered `false static noexcept( true )`, then there is a potential that UB can cause program termination by "leaking" an exception out from checked try blocks. - If UB is considered `false static noexcept( false )`, then it makes writing `false static noexcept( true )` extremely difficult. Many situations will require noisy and defensive `try { /* code */ } catch( ... ) { /* silently ignore exception and do nothing */ }` patterns. The conclusion about whether language UB should be considered `false static noexcept( true )` for the purpose of this checking is irrelevant, as this form exists merely for expository purposes. #### Rewriting the original painful example using this feature ~~~ void silentlyThrowing() noexcept( false ) { throw 0; } void wrappedSilentlyThrowing() noexcept( true ) { silentlyThrowing(); } // We try to be more responsible void tickingTimeBomb() static noexcept( true ) try { if( not ( rand() % 32 ) ) { silentlyThrowing(); } if( not ( rand() % 32 ) ) { wrappedSilentlyThrowing(); } } catch( ... ) { } enum Result { Success, Failure }; Result illFormedCannotTolerateExceptions() false static noexcept( true ) { try { auto element= someVector.at( 42 ); someVector.push_back( std::move( element ) ); } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } // This line is ill formed and has to be moved into the try block to satisfy the `false static noexcept` // requirements tickingTimeBomb(); return Success; } Result cannotTolerateExceptions() false static noexcept( true ) { try { auto element= someVector.at( 42 ); someVector.push_back( std::move( element ) ); tickingTimeBomb(); } catch( ... ) { try { std::cerr << "Something went wrong" << std::endl; } catch( ... ) { /* Failure to log is troublesome but not fatal */ } return Failure; } return Success; } ~~~