Allowing exception throwing in constant-evaluation
Introduction
Since adding the constexpr
keyword in C++11, WG21 has gradually expanded the scope of language features for use in constant-evaluated code. At first, users couldn't even use if
, else
, or loops. C++14 added support for them. C++17 added support for constexpr lambdas. C++20 finally added the ability to use allocation, std::vector
and std::string
. These improvements have been widely appreciated, and lead to simpler code that doesn't need to work around differences between normal and constexpr C++.
The last major language feature from C++ still not present in constexpr code is the ability to throw an exception. This absence forces library authors to use more intrusive error reporting mechanisms. One example would be the use of std::expected
and std::optional
. Another one is the complete omission of error handling. This leaves users with long and confusing errors generated by the compiler.
Throwing exceptions in constant evaluated code is the preferred way of error reporting in the proposal adding Static Reflection for C++26. Some meta-functions can fail, and allowing them to throw will significantly simplify reflection code.
Changes
- R4 → R5: Marking a note part of the wording as addition. Move wording about lifetime of exceptions and implicit copies to expr.const p5.
- R3 → R4: Added missing library exception type
bad_typeid
. Added wording to specify literal encoding ofstd::exception::what()
member function's result during constant evaluation. - R2 → R3: Added library exception types
bad_alloc
,bad_array_new_length
, andbad_typeid
, removed recommended practice in the language wording part, remove part of language wording to allow exception throwing fromdynamic_cast
andtypeid
. Adding feature test macro, moving language and library wording to bottom of the paper. - R1 → R2: Added library wording, new examples, implementation description, and updated impact of changes.
- R0 → R1: Changed wording after consultation with Jens, added example to show exception can't leak via
std::exception_ptr
Motivational example
consteval auto hello(std::string_view input) {
if (input.empty()) {
throw invalid_argument{"empty name provided"}; // BEFORE: compile-time error at throw expression site when reached
}
return concat_into_a_fixed_string("hello ",input);
}
const auto a = hello(""); // AFTER: compile-time error at call site "empty name provided"
const auto b = hello("Hana");
try {
const auto c = hello(""); // AFTER: this exception is caught
} catch (const validation_error &) {
// everything is fine
}
const auto d = hello("Adolph Blaine Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Paul Quincy Randolph Sherman Thomas Uncas Victor William Xerxes Yancy Zeus Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflegeundsorgfaltigkeitbeschutzenvonangreifendurchihrraubgierigfeindewelchevoralternzwolftausendjahresvorandieerscheinenvanderersteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenachdiesternwelchegehabtbewohnbarplanetenkreisedrehensichundwohinderneurassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemitnichteinfurchtvorangreifenvonandererintelligentgeschopfsvonhinzwischensternartigraum."); // BEFORE: compile-time error at throw expression site deep in the concat function
const auto d = hello("Adolph Blaine Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Paul Quincy Randolph Sherman Thomas Uncas Victor William Xerxes Yancy Zeus Wolfeschlegelsteinhausenbergerdorffwelchevoralternwarengewissenhaftschaferswessenschafewarenwohlgepflegeundsorgfaltigkeitbeschutzenvonangreifendurchihrraubgierigfeindewelchevoralternzwolftausendjahresvorandieerscheinenvanderersteerdemenschderraumschiffgebrauchlichtalsseinursprungvonkraftgestartseinlangefahrthinzwischensternartigraumaufdersuchenachdiesternwelchegehabtbewohnbarplanetenkreisedrehensichundwohinderneurassevonverstandigmenschlichkeitkonntefortpflanzenundsicherfreuenanlebenslanglichfreudeundruhemitnichteinfurchtvorangreifenvonandererintelligentgeschopfsvonhinzwischensternartigraum."); // AFTER: compile-time error at call site "too long name provided"
Implementation experience
A partial implementation in Clang is available on compiler-explorer.com. I expect it to be feature complete in time for the presentation at the St. Louis meeting.
The implementation approach is an exception slot to for throw
-statements to store it in the same way as return
-statements do. Every try
block creates a new exception slot and try to match the appropriate catch
block.
Library support is implemented via a set of new compiler builtins.
Three other compiler vendors were consulted and raised no implementability concerns.
Impact on existing code
This change doesn't break any existing code as throwing exceptions without catching them is already an error and is used by various libraries (fmt, CTHASH) to improve compile-time errors.
This proposal can make previously non-compiling code which reached a throw
-statement compile by catching the thrown exception.
The intent is to keep this useful mechanism intact. The proposed wording change will only modify behavior in cases where a try
/catch
block is present.
Examples
Parsing date
constexpr date parse_date(std::string_view input) {
auto [correct, year, month, day] = ctre::match<"([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})">(input);
if (!correct) {
throw incorrect_date{input};
}
return build_date(year, month, day);
}
constexpr auto birthday = parse_date("1991-07-21"); // fine
constexpr auto wrong_day = parse_date("1-2-3"); // COMPILE-TIME ERROR: provided incorrect date "1-2-3"
constexpr auto weird_day = parse_date("2023-03-29"); // COMPILE-TIME ERROR: year 2023 doesn't have a leap day
Non-transient exceptions
The current wording matches pre-P3032 wording and treats an exception basically as an allocation, and such allocation can't escape any constexpr
variable initialization.
constexpr auto just_error() {
throw my_exception{};
}
constexpr void foo() {
try {
auto v = just_error(); // OK
} catch (my_exception) {
// do nothing
}
try {
constexpr auto v = just_error(); // COMPILE-TIME ERROR: uncaught exception of type "my_exception"
} catch (my_exception) { }
}
Exceptions must be caught
constexpr unsigned divide(unsigned n, unsigned d) {
if (d == 0u) {
throw invalid_argument{"division by zero"};
}
return n / d;
}
constexpr auto b = divide(5, 0); // BEFORE: compilation error due reaching a throw expression
constexpr auto b = divide(5, 0); // AFTER: still a compilation error but due the uncaught exception
constexpr std::optional<unsigned> checked_divide(unsigned n, unsigned d) {
try {
return divide(n, d);
} catch (...) {
return std::nullopt;
}
}
constexpr auto a = checked_divide(5, 0); // BEFORE: compilation error
constexpr auto a = checked_divide(5, 0); // AFTER: std::nullopt value
Constant evaluation violation behavior won't be changed
This paper doesn't propose throwing an exception for any other constant evaluation error.
constexpr unsigned read_pointer(const unsigned* p) {
return *p;
}
constexpr unsigned try_to_ignore_ub() {
try {
return read_pointer(nullptr);
} catch (...) {
return 0;
}
}
constexpr auto v = try_to_ignore_ub(); // UNCHANGED: error due reaching UB by dereferencing the NULL pointer (no exception is involved)
Lifetime of exception object must stay inside constant evaluation
Exception objects thrown at compile-time must be caught before leaving their respective constant-evaluated scope, and can't be stored for run-time access. This is similar to constexpr allocations, which must be deallocated before leaving their respective scopes, and any memory that's reserved at compile-time is inaccessible to run-time evaluations.
consteval auto fail() -> std::exception_ptr{
try {
throw failure{};
} catch (...) {
return std::current_exception();
}
}
constexpr auto stored_exception = fail(); // NOT ALLOWED to store std::exception_ptr in a constexpr variable
Special thanks
To Richard Smith, Barry Revzin, Daveed Vandevoorde, Jana Machutová, Christopher Di Bella, Jens Maurer, Robert C. Seacord, Lewis Baker, David Sankel, and Guy Davidson.Wording
7.7 Constant expressions [expr.const]
(5) An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:
(5.2) a control flow that passes through a declaration of a block variable ([basic.scope.block]) with static ([basic.stc.static]) or thread ([basic.stc.thread]) storage duration, unless that variable is usable in constant expressions;
[Example 1:
constexpr char test() {
static const int x = 5;
static constexpr char c[] = "Hello World";
return *(c + x);
}
static_assert(' ' == test());
— end example]
(5.?) a construction of an exception object, unless the exception object and all of its implicit copies created by invocations of std::current_exception
or std::rethrow_exception
([propagation]) are destroyed within the evaluation of E;
(5.25) a throw-expression ([expr.throw]);
(5.26) a dynamic_cast
([expr.dynamic.cast]) or typeid
([expr.typeid]) expression on a glvalue that refers to an object whose dynamic type is constexpr-unknown or that would throw an exception;
(5.?) a dynamic_cast
([expr.dynamic.cast]) expression, typeid
([expr.typeid]) expression, or new-expression ([expr.new]) that would throw an exception where no definition of the exception type is reachable;
Library wording
Note: This paper doesn't contain wording for any <stdexcept>
changes as these are proposed already in P3295: Freestanding constexpr containers and constexpr exception types and P3378: constexpr exception types
17.6.4.1 Class bad_alloc
[bad.alloc]
namespace std {
class bad_alloc : public exception {
public:
// see [exception] for the specification of the special member functions
constexpr const char* what() const noexcept override;
};
}
constexpr const char* what() const noexcept override;
17.6.4.1 Class bad_array_new_length
[new.badlength]
namespace std {
class bad_array_new_length : public bad_alloc {
public:
// see [exception] for the specification of the special member functions
constexpr const char* what() const noexcept override;
};
}
constexpr const char* what() const noexcept override;
17.7.4 Class bad_cast
[bad.cast]
namespace std {
class bad_cast : public exception {
public:
// see [exception] for the specification of the special member functions
constexpr const char* what() const noexcept override;
};
}
constexpr const char* what() const noexcept override;
17.7.5 Class bad_typeid
[bad.typeid]
namespace std {
class bad_typeid : public exception {
public:
// see [exception] for the specification of the special member functions
constexpr const char* what() const noexcept override;
};
}
constexpr const char* what() const noexcept override;
17.9.2 Header <exception> synopsis [exception.syn]
// all freestanding
namespace std {
class exception;
class bad_exception;
class nested_exception;
using terminate_handler = void (*)();
terminate_handler get_terminate() noexcept;
terminate_handler set_terminate(terminate_handler f) noexcept;
[[noreturn]] void terminate() noexcept;
constexpr int uncaught_exceptions() noexcept;
using exception_ptr = unspecified;
constexpr exception_ptr current_exception() noexcept;
[[noreturn]] constexpr void rethrow_exception(exception_ptr p);
template<class E> constexpr exception_ptr make_exception_ptr(E e) noexcept;
template<class T> [[noreturn]] constexpr void throw_with_nested(T&& t);
template<class E> constexpr void rethrow_if_nested(const E& e);
}
17.9.3 Class exception [exception]
namespace std {
class exception {
public:
constexpr exception() noexcept;
constexpr exception(const exception&) noexcept;
constexpr exception& operator=(const exception&) noexcept;
constexpr virtual ~exception();
constexpr virtual const char* what() const noexcept;
};
}
1The class exception
defines the base class for the types of objects thrown as exceptions by C++ standard library components, and certain expressions, to report errors detected during program execution.
2Except where explicitly specified otherwise, each standard library class T
that derives from class exception
has the following publicly accessible member functions, each of them having a non-throwing exception specification ([except.spec]):
- (2.1)default constructor (unless the class synopsis shows other constructors)
- (2.2)copy constructor
- (2.3)copy assignment operator
lhs
and rhs
both have dynamic type T
and lhs
is a copy of rhs
, then strcmp(lhs.what(), rhs.what())
is equal to 0
. The what()
member function of each such T
satisfies the constraints specified for exception::what()
(see below).constexpr exception(const exception& rhs) noexcept;
constexpr exception& operator=(const exception& rhs) noexcept;
3Postconditions: If *this
and rhs
both have dynamic type exception then the value of the expression strcmp(what(), rhs.what())
shall equal 0
.
constexpr virtual ~exception();
4Effects: Destroys an object of class exception.
constexpr virtual const char* what() const noexcept;
5Returns: An implementation-defined ntbs, which during constant evaluation is encoded with the ordinary literal encoding ([lex.ccon]).
6Remarks: The message may be a null-terminated multibyte string, suitable for conversion and display as a wstring
([string.classes], [locale.codecvt]). The return value remains valid until the exception object from which it is obtained is destroyed or a non-const
member function of the exception object is called.
17.9.4 Class bad_exception [bad.exception]
namespace std {
class bad_exception : public exception {
public:
// see [exception] for the specification of the special member functions
constexpr const char* what() const noexcept override;
};
}
constexpr virtual const char* what() const noexcept override;
17.9.6 uncaught_exceptions [uncaught.exceptions]
constexpr int uncaught_exceptions() noexcept;
17.9.7 Exception propagation [propagation]
using exception_ptr = unspecified;
(8) All member functions are marked constexpr.
constexpr exception_ptr current_exception() noexcept;
[[noreturn]] constexpr void rethrow_exception(exception_ptr p);
template<class E> constexpr exception_ptr make_exception_ptr(E e) noexcept;
17.9.8 nested_exception [except.nested]
namespace std {
class nested_exception {
public:
constexpr nested_exception() noexcept;
constexpr nested_exception(const nested_exception&) noexcept = default;
constexpr nested_exception& operator=(const nested_exception&) noexcept = default;
constexpr virtual ~nested_exception() = default;
// access functions
[[noreturn]] constexpr void rethrow_nested() const;
constexpr exception_ptr nested_ptr() const noexcept;
};
template<class T> [[noreturn]] constexpr void throw_with_nested(T&& t);
template<class E> constexpr void rethrow_if_nested(const E& e);
}
constexpr nested_exception() noexcept;
[[noreturn]] constexpr void rethrow_nested() const;
constexpr exception_ptr nested_ptr() const noexcept;
template<class T> [[noreturn]] constexpr void throw_with_nested(T&& t);
template<class E> constexpr void rethrow_if_nested(const E& e);
Feature test macro
15.11 Predefined macro names [cpp.predefined]
__cpp_constexpr_exceptions 2024??L
17.3.2 Header <version> synopsis [version.syn]
#define __cpp_lib_constexpr_exceptions 2024??L