constexpr exception types

This paper is mechanical wording change; making all exception types associated with constexpr compatible functionality marked constexpr. This is needed for consistency across library since allowing exception handling during constant evaluation by P3068.

As every exception type is just an ordinary type, they should be make constexpr compatible.

TL;DR

Now when we can throw exceptions during constant evaluation we should make sure all constexpr compatible functionality (eg. vector) is able to throw its exception types (eg. out_of_range) and allow users to recover from errors gracefully.

This proposal fixes the lag between constexpr exception support and library constexpr support and all future constexprification papers should make their exception types constexpr.

Implementation experience

Implemented in libc++ as part of implementation of P3068, with source code available on my github and with compiler + library available on the compiler explorer.

constexpr bool check(const char * msg) {
    try {
        auto vec = std::vector<int>{0,1,2,3};
        return vec.at(4) == 4; // out-of-range
    } catch (const std::out_of_range & exc) {
        return std::string_view{exc.what()} == msg;
    } catch (...) {
        return false;
    }
    return false;
}

static_assert(check("vector"));
assert(check("vector"));

libc++

In libc++ most of exception types have implementation of what() function members and destructors inside .cpp files. Most of them are in form:


		constexpr const char* EXCEPTION_TYPE::what() const noexcept { return "EXCEPTION_MESSAGE"; }
		constexpr ~EXCEPTION_TYPE() noexcept { /* do nothing */ }
	

This code needs to be moved to header files.

Reference counted string

One obstacle was a reference counted string inside logic_error and runtime_error which allocates sizeof(_Rep_base) + strlen(_string) + 1 byte storage, and is using reinterpret_cast to access the _Rep_base. Also it uses atomic operations for refcounting.

This can be avoided by not doing this during constant evaluation and just copy the string everytime underlying exception is copied or assigned.

libc++abi

By moving exception member functions implementation to header files out of .cpp files we are loosing existing symbol emitted inside shared library of libc++ and libc++abi. This needs to be carefully fixed by providing same symbol explicitly to keep compatibility.

Dependency on <string>

All library generic errors (based on logic_error and runtime_error) are using also std::string constructor. But <stdexcept> is also required by <string>. This creates a cycle of dependency and fix is moving implementation of <stdexcept> constructors using std::string after definition of std::basic_string<CharT> template.

Existing exception types

Following table shows in detail what is proposed and what is not. Also it shows which standard library implementation has exception types implemented in headers and which not. In addition to it you can see if functionality throwing the exception is already constant evaluatable or not. Allowing associated exception types to be constant evaluatable will allow users to recover from errors.

exception
type
defined
in
constexprimplemented in headernote
exception
itself
associated
functionality
libc++libstdc++STL
language exception<exception>in progressin progressxx✔︎P3068, base class for all standard exceptions
bad_alloc<new>in progress✔︎xx✔︎P3068
bad_array_new_length<new>in progress✔︎xx✔︎P3068
bad_cast<typeinfo>in progress✔︎xx✔︎P3068
bad_exception<exception>in progressin progressxx✔︎P3068
bad_typeid<typeinfo>in progress✔︎xx✔︎P3068
library generic errors domain_error<stdexcept>proposed–xx✔︎
invalid_argument<stdexcept>proposed–xx✔︎
length_error<stdexcept>proposed–xx✔︎
logic_error<stdexcept>proposed–xx✔︎base class for others
out_of_range<stdexcept>proposed–xx✔︎
overflow_error<stdexcept>proposed–xx✔︎
range_error<stdexcept>proposed–xx✔︎
runtime_error<stdexcept>proposed–xx✔︎base class for others
underflow_error<stdexcept>proposed–xx✔︎
library errors bad_any_cast<any>not proposedxx✔︎✔︎
bad_expected_access<T><expected>proposed✔︎x✔︎✔︎
bad_function_call<functional>not proposedxxx✔︎
bad_optional_access<optional>proposed✔︎x✔︎✔︎
bad_variant_access<variant>proposed✔︎x✔︎✔︎
bad_weak_ptr<memory>in progressin progressxx✔︎P3037
ios_base::failure<ios>not proposedxxx✔︎
filesystem_error<filesystem>not proposedxxx✔︎
future_error<future>not proposedxxx✔︎
chrono::nonexistent_local_time<chrono>not proposedxx✔︎✔︎uses ostringstream
chrono::ambiguous_local_time<chrono>not proposedxx✔︎✔︎uses ostringstream
regex_error<regex>not proposedxxx✔︎
format_error<format>proposed✔︎x✔︎✔︎
system_error<system_error>not proposedxxx✔︎

1 already proposed in a different proposal.
2 MS STL is defining some exception types in <exception> instead of header where they are supposed to be defined.
3 not needed unless functionality using the exception type is also constexpr compatible.
4 this functionality will probably never be constexpr compatible as it needs to observe outside of its program (is doing I/O or gives you current time).
5 will need to implement changes to error_code and error_category.

Should all exception types be constexpr?

In general, yes. But I'm not proposing it. But in future every constexprification proposal should make associated exception types constexpr compatible.

std::runtime_error

Yes, this exception should be constexpr too, as it's used as base classes for other exception types (eg. out_of_range). Maybe the name sounds funny in constant evaluation context, but it is really needed to be constexpr compatible.

std::error_code and std::error_category depending exception types

In future to make all exception types constant evaluatable we will need to make parts of std::error_code and std::error_category constexpr. List of these types and their dependencies:

  • filesystem_error — error_code only
  • system_error — both error_code and error_category
  • ios_base::failure — error_code only

Impact on existing code

Pure extension, previously types weren't compatible with constant evaluation. For standard libraries it needs to be implemented carefully to not break ABI.

Intention for wording changes

Mark all function members, constructors, and destructors of all following exception types with constexpr:

  • logic_error,
  • domain_error,
  • invalid_argument,
  • length_error,
  • out_of_range,
  • runtime_error,
  • range_error,
  • overflow_error,
  • underflow_error,
  • bad_optional_access,
  • bad_variant_access,
  • bad_expected_access,
  • format_error

In addition to this change, some .what() members mentions they are returning implementation-defined NTBS and these strings needs to be in ordinary literal encoding (as recommended by SG16 for P3068: Allowing exception throwing in constant-evaluation).

Proposed changes to wording

19.2.3 Class logic_error [logic.error]

namespace std { class logic_error : public exception { public: constexpr explicit logic_error(const string& what_arg); constexpr explicit logic_error(const char* what_arg); }; }
The class logic_error defines the type of objects thrown as exceptions to report errors presumably detectable before the program executes, such as violations of logical preconditions or class invariants.
constexpr logic_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr logic_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.4 Class domain_error [domain.error]

namespace std { class domain_error : public logic_error { public: constexpr explicit domain_error(const string& what_arg); constexpr explicit domain_error(const char* what_arg); }; }
The class domain_error defines the type of objects thrown as exceptions by the implementation to report domain errors.
constexpr domain_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr domain_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.5 Class invalid_argument [invalid.argument]

namespace std { class invalid_argument : public logic_error { public: constexpr explicit invalid_argument(const string& what_arg); constexpr explicit invalid_argument(const char* what_arg); }; }
The class invalid_argument defines the type of objects thrown as exceptions to report an invalid argument.
constexpr invalid_argument(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr invalid_argument(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.6 Class length_error [length.error]

namespace std { class length_error : public logic_error { public: constexpr explicit length_error(const string& what_arg); constexpr explicit length_error(const char* what_arg); }; }
The class length_error defines the type of objects thrown as exceptions to report an attempt to produce an object whose length exceeds its maximum allowable size.
constexpr length_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr length_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.7 Class out_of_range [out.of.range]

namespace std { class out_of_range : public logic_error { public: constexpr explicit out_of_range(const string& what_arg); constexpr explicit out_of_range(const char* what_arg); }; }
The class out_of_range defines the type of objects thrown as exceptions to report an argument value not in its expected range.
constexpr out_of_range(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr out_of_range(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.8 Class runtime_error [runtime.error]

namespace std { class runtime_error : public exception { public: constexpr explicit runtime_error(const string& what_arg); constexpr explicit runtime_error(const char* what_arg); }; }
The class runtime_error defines the type of objects thrown as exceptions to report errors presumably detectable only when the program executes.
constexpr runtime_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr runtime_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.9 Class range_error [range.error]

namespace std { class range_error : public runtime_error { public: constexpr explicit range_error(const string& what_arg); constexpr explicit range_error(const char* what_arg); }; }
The class range_error defines the type of objects thrown as exceptions to report range errors in internal computations.
constexpr range_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr range_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.10 Class overflow_error [overflow.error]

namespace std { class overflow_error : public runtime_error { public: constexpr explicit overflow_error(const string& what_arg); constexpr explicit overflow_error(const char* what_arg); }; }
The class overflow_error defines the type of objects thrown as exceptions to report an arithmetic overflow error.
constexpr overflow_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr overflow_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

19.2.11 Class underflow_error [underflow.error]

namespace std { class underflow_error : public runtime_error { public: constexpr explicit underflow_error(const string& what_arg); constexpr explicit underflow_error(const char* what_arg); }; }
The class underflow_error defines the type of objects thrown as exceptions to report an arithmetic underflow error.
constexpr underflow_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr underflow_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

22.5.5 Class bad_optional_access [optional.bad.access]

namespace std { class bad_optional_access : public exception { public: // see [exception] for the specification of the special member functions constexpr const char* what() const noexcept override; }; }
The class bad_optional_access defines the type of objects thrown as exceptions to report the situation where an attempt is made to access the value of an optional object that does not contain a value.
constexpr const char* what() const noexcept override;
Returns: An implementation-defined ntbs, which during constant evaluation shall be encoded with the ordinary literal encoding ([lex.ccon]).
note for editor: this change is mirroring the change recommended by SG16 for P3068.

22.6.11 Class bad_variant_access [variant.bad.access]

namespace std { class bad_variant_access : public exception { public: // see [exception] for the specification of the special member functions constexpr const char* what() const noexcept override; }; }
Objects of type bad_variant_access are thrown to report invalid accesses to the value of a variant object.
constexpr const char* what() const noexcept override;
Returns: An implementation-defined ntbs, which during constant evaluation shall be encoded with the ordinary literal encoding ([lex.ccon]).
note for editor: this change is mirroring the change recommended by SG16 for P3068.

22.8.4 Class template bad_expected_access [expected.bad]

namespace std { template<class E> class bad_expected_access : public bad_expected_access<void> { public: constexpr explicit bad_expected_access(E); constexpr const char* what() const noexcept override; constexpr E& error() & noexcept; constexpr const E& error() const & noexcept; constexpr E&& error() && noexcept; constexpr const E&& error() const && noexcept; private: E unex; // exposition only }; }
The class template bad_expected_access defines the type of objects thrown as exceptions to report the situation where an attempt is made to access the value of an expected<T, E> object for which has_value() is false.
constexpr explicit bad_expected_access(E e);
Effects: Initializes unex with std​::​move(e).
constexpr const E& error() const & noexcept; constexpr E& error() & noexcept;
Returns: unex.
constexpr E&& error() && noexcept; constexpr const E&& error() const && noexcept;
Returns: std​::​move(unex).
constexpr const char* what() const noexcept override;
Returns: An implementation-defined ntbs, which during constant evaluation shall be encoded with the ordinary literal encoding ([lex.ccon]).
note for editor: this change is mirroring the change recommended by SG16 for P3068.

22.8.5 Class template specialization bad_expected_access<void> [expected.bad.void]

namespace std { template<> class bad_expected_access<void> : public exception { protected: constexpr bad_expected_access() noexcept; constexpr bad_expected_access(const bad_expected_access&) noexcept; constexpr bad_expected_access(bad_expected_access&&) noexcept; constexpr bad_expected_access& operator=(const bad_expected_access& noexcept); constexpr bad_expected_access& operator=(bad_expected_access&&) noexcept; constexpr ~bad_expected_access(); public: constexpr const char* what() const noexcept override; }; }
constexpr const char* what() const noexcept override;
Returns: An implementation-defined ntbs, which during constant evaluation shall be encoded with the ordinary literal encoding ([lex.ccon]).
note for editor: this change is mirroring the change recommended by SG16 for P3068.

22.14.10 Class format_error [format.error]

namespace std { class format_error : public runtime_error { public: constexpr explicit format_error(const string& what_arg); constexpr explicit format_error(const char* what_arg); }; }
The class format_error defines the type of objects thrown as exceptions to report errors from the formatting library.
constexpr format_error(const string& what_arg);
Postconditions: strcmp(what(), what_arg.c_str()) == 0.
constexpr format_error(const char* what_arg);
Postconditions: strcmp(what(), what_arg) == 0.

Feature test macro

17.3.2 Header <version> synopsis [version.syn]

#define __cpp_lib_constexpr_exception_types 2024??L