Files
cpp-proposals/double-static-noexcept.md
2026-07-16 17:44:36 -04:00

591 lines
20 KiB
Markdown

# `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;
}
~~~