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

20 KiB

double static noexcept -- a mechanism for static checking of noexcept blocks

Table of Contents

  1. Abstract
  2. Discussion
  3. Proposal
  4. 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
  4. 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;
}