20 KiB
double static noexcept -- a mechanism for static checking of noexcept blocks
Table of Contents
- Abstract
- Discussion
- Proposal
- 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:
- An evaluation of
noexcept( someStaticNoexceptFunction )will evaluate astrue. - An evaluation of
static noexcept( someStaticNoexceptFunction )will evaluate astrue. - An evaluation of
static noexcept( someClassicNoexceptFunction )will evaluate asfalse. - 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 atryblock, then the program is ill formed, and a compile-time diagnostic is required. - If a
noexcept( false )function is called inside the body of atryblock, then acatch( ... )block must exist. - A
catchblock's body is not considered part of itstryblock. - When checking if a statement is "in" a
tryblock, one must walk up the nested block structure until a try block is reached. Acatchblock attached to atryblock 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 atryblock 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
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
noexcept( false ), then it makes writingstatic noexcept( true )extremely difficult. Many situations will require noisy and defensivetry { /* code */ } catch( ... ) { /* silently ignore exception and do nothing */ }patterns.
- If UB is considered
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:
- An evaluation of
noexcept( someFalseStaticNoexceptFunction )will evaluate astrue. - An evaluation of
static noexcept( someFalseStaticNoexceptFunction )will evaluate astrue. - An evaluation of
false static noexcept( someClassicNoexceptFunction )will evaluate asfalse. - An evaluation of
false static noexcept( someStaticNoexceptFunction )will evaluate asfalse. - An evaluation of
false static noexcept( someFalseStaticNoexceptFunction )will evaluate astrue. - A call to a
false static noexcept( true )function will not throw. - 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:
- If a
false static noexcept( false )function is called outside of the body of atryblock, then the program is ill formed, and a compile-time diagnostic is required. - If a
false static noexcept( false )function is called inside the body of atryblock, then acatch( ... )block must exist. - A
catchblock's body is not considered part of itstryblock. - When checking if a statement is "in" a
tryblock, one must walk up the nested block structure until a try block is reached. Acatchblock attached to atryblock 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
false static noexcept( false )functions is ill formed unless atryblock 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
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
noexcept( false ), then it makes writingstatic noexcept( true )extremely difficult. Many situations will require noisy and defensivetry { /* code */ } catch( ... ) { /* silently ignore exception and do nothing */ }patterns.
- If UB is considered
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:
- An evaluation of
noexcept( someStaticNoexceptFunction )will evaluate astrue. - An evaluation of
static noexcept( someStaticNoexceptFunction )will evaluate astrue. - An evaluation of
static noexcept( someClassicNoexceptFunction )will evaluate asfalse. - 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.
=-=-=-=-=-=-=-=-=-=-
double static noexcept -- a mechanism for static checking of noexcept blocks
Table of Contents
- Abstract
- Discussion
- Proposal
- 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 astrue. - An evaluation of
static noexcept( someStaticNoexceptFunction )will evaluate astrue. - An evaluation of
static noexcept( someClassicNoexceptFunction )will evaluate asfalse. - 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 atryblock, then the program is ill formed, and a compile-time diagnostic is required. - If a
noexcept( false )function is called inside the body of atryblock, then acatch( ... )block must exist. - A
catchblock's body is not considered part of atryblock. - When checking if a statement is "in" a
tryblock, one must walk up the nested block structure until a try block is reached. Acatchblock attached to atryblock 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 atryblock 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 writingfalse static noexcept( true )extremely difficult. Many situations will require noisy and defensivetry { /* code */ } catch( ... ) { /* silently ignore exception and do nothing */ }patterns.
- If UB is considered
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;
}