constexpr
<exceptions>
and <stdexcept>
parts, as those are already in the standard.stdexcept
exceptions.operator delete
.constexpr
.
This proposal helps enable P2996 reflection and more advanced metaprogramming for freestanding environments.
Specifically, this allows std::vector
, std::string
, and std::allocator
to be used in constant evaluated contexts in freestanding.
As a first approximation, this makes the vector
and string
facilities consteval
in freestanding.
Freestanding implementations that know more about their targets are permitted to provide the full constexpr
facilities if they wish.
This paper is attempting to modify the minimum set of facilities possible to freestanding in order to enable reflection, plus some additional exception types that come along for "free".
Freestanding applications are often unable to allocate memory or throw exceptions at runtime. No such restriction exists while building the application though.
Reflection in P2996R2 contains many consteval
functions that return vector<meta::info>
, such as members_of
.
Those facilities are valuable in freestanding and hosted alike.
This paper would "only" make vector
and allocator
consteval on freestanding if it reasonably could as a means to limit scope, but vector
has functions that throw length_error
and out_of_range
.
length_error
and out_of_range
are not in freestanding prior to this paper, so mentioning them renders the program ill-formed, even in a constant evaluation context.
This paper therefore makes those exception types consteval
on freestanding.
Similarly, length_error
and out_of_range
mention string
, which is also not yet in freestanding.
Therefore the scope expands to string
.
Fortunately, char_traits
and most of string_view
are already freestanding.
All the <stdexcept>
exceptions are receiving the same consteval
treatment, since the classes are largely identical other than the class names.
The freestanding-deleted methods of string_view
will need to be made consteval
on freestanding in order to support string
s use cases.
freestanding-consteval additions:
allocator
string_view
freestanding-deleted membersstring
vector
logic_error
domain_error
invalid_argument
length_error
out_of_range
runtime_error
range_error
overflow_error
underflow_error
constexpr
things?
In P2268 Freestanding Roadmap, a strategy is discussed of making everything that is constexpr
in the standard library available at compile time via consteval
, and those facilities that are also suitable at runtime to be marked constexpr
and // freestanding
.
That is still the desired goal.
Reaching that goal requires a lot of research and testing.
There are likely other, non-constexpr
facilities that the constexpr
facilities rely on that would need to be audited as well.
Destructors cannot currently be marked as consteval
.
virtual
functions that override constexpr
methods cannot currently be marked as consteval
.
Those restrictions also prevent us from blanket consteval
'ing all the constexpr
facilities in the standard library.
This paper has chosen to prioritize the reflection subset of that work, rather than block progress on freestanding reflection.
consteval
functions are never evaluated during runtime, but constexpr
functions are sometime evaluated during runtime.
A freestanding library using a consteval
facility may end up "downgrading" some code from compile time to runtime during a port to a hosted environment due to the change to constexpr
.
This can result in worse performance due to the runtime function calls and allocations that were previously compile time function calls and allocations.
In this unoptimized example, we get runtime calls to an allocating constructor and deallocating destructor in the constexpr
version compared to the consteval
version.
For this example, the difference disappears when optimizations are enabled, but that won't always be the case.
constexpr
allocations
Non-transient allocation with vector and basic_string is attempting to make it possible to allow constant evaluated vector
s and string
s persist into runtime.
I'd like to avoid causing issues for non-transient allocation papers.
We can't rely on the future success of P3544 though. As a result this paper is making the entirety of vector
and basic_string
freestanding-consteval. One non-transient allocation is here, there will be a desire to make non-throwing const
methods constexpr
on freestanding, rather than freestanding-consteval.
This would make it possible to observe the values in containers with non-transient allocations possible at runtime.
We would still be able to avoid allocations and exceptions at runtime, while also allowing the constant compile time object to be observed.
Instead, we could have marked the functions as constexpr
in this paper. One unfortunate aspect of doing so is that it would make operator[]
, front()
, and back()
as potentially callable at runtime, even though the only possible runtime containers are empty.
LEWG should discuss whether we should prioritize future-proofing the containers or mistake-proofing them.
For now, the paper has chosen to mistake-proof.
operator delete
and default constructed containers
Even when marked as consteval
, a default constructed std::string
or std::vector
can persist into runtime.
The destructor for such an object can be executed at runtime.
That destructor will run operator delete
on a nullptr
.
Freestanding implementations may not have a heap [new.delete.general]#2.
Conforming implementations aren't required to have a useable operator new
, but they are required to have an operator delete
that can handle a nullptr
.
That means that it is acceptable (though unfortunate) that an operator delete
"escapes" into runtime.
In practice, I don't think this will be a big problem, as there isn't much that can be done with a default constructed runtime container where all the mutating operations are consteval
.
Ideally, this scenario would be ill-formed.
The author has proposed P3421: Consteval Destructors which would improve this situation.
consteval
destructors aren't a requirement for this paper though.
Is std::allocator
still an allocator if it can only allocate
and deallocate
at compile time?
In practice, this seems to work fine in code. So I'm leaving the requirements alone. This paper doesn't make any changes to the named requirements for allocators or allocator_traits.
Maybe in the future the standard will be more explicit about when library facilities need to be available at compile time vs. run time.
This proposal has been implemented in the Microsoft STL implementation, targeting the Microsoft Windows kernel.
The binaries were compiled with clang-cl, as Microsoft's cl doesn't yet implement P2564: consteval needs to propagate up.
consteval
propagating up was necessary for compressed pair implementations.
Some changes had to be made to vcruntime to move some std::exception
code inline.
The libc++ test suite was used to verify the changes. I modified the tests to verify the compile-time paths (if it wasn't doing so already), and remove the run-time paths. I would expect production implementations to do this with an `#ifdef` to remove the run-time paths when testing the freestanding implementation.
I will note that the Microsoft Windows kernel implementation does not support C++ exceptions. As a result, the tests did not exercise any throwing paths.
P3160R2 "An Allocator-Aware inplace_vector" and P3418R0 Wording for P3160R2 inplace_vector's "construct/destroy" alternative
needd to make allocators freestanding, since inplace_vector
is already freestanding.
inplace_vector
doesn't directly need the allocate
and deallocate
methods in the allocator.
This paper needs those methods, but only during constant evaluation.
For allocator wording ([allocator.requirements.general] and [allocator.traits.memgers]), this paper's wording matches draft versions of P3160.
This paper's freestanding-consteval changes in [default.allocator] use a different wording technique than the inplace_vector
papers, but the intent is the same.
Instructions to the editor:
Move [functions.within.classes] below [freestanding.item]. This is so we can use the "freestanding consteval classes" term after it is defined. Then modify [functions.within.classes] as follows:
For the sake of exposition, [support] through [exec] and [depr] do not describe copy/move constructors, assignment operators, or (non-virtual) destructors with the same apparent semantics as those that can be generated by default ([class.copy.ctor], [class.copy.assign], [class.dtor]).It is unspecified whether the implementation provides explicit definitions for such member function signatures, or for virtual destructors that can be generated by default.On freestanding implementations, it is implementation defined whether the copy/move constructors and assignment operators of freestanding consteval classes are immediate functions or constexpr functions.
Function declarations and function template declarations followed by a comment that include freestanding-deleted are freestanding deleted functions.On freestanding implementations, it is implementation-defined whether each entity introduced by a freestanding deleted function is a deleted function ([dcl.fct.def.delete]) or whether the requirements are the same as the corresponding requirements for a hosted implementation.[Note 1:Deleted definitions reduce the chance of overload resolution silently changing when migrating from a freestanding implementation to a hosted implementation.— end note][Example 1: double abs(double j); // freestanding-deleted — end example]Function declarations and function template declarations followed by a comment that includes freestanding-consteval are freestanding consteval functions.On freestanding implementations, it is implementation-defined whether each entity introduced by a freestanding consteval function is an immediate function ([expr.const]) or whether the requirements are the same as the corresponding requirements for a hosted implementation.Class declarations and class template declarations followed by a comment that includes freestanding-consteval are freestanding consteval classes./*...*/A declaration in a synopsis is a freestanding item if
- it is followed by a comment that includes freestanding,
- it is followed by a comment that includes freestanding-deleted, or
- it is followed by a comment that includes freestanding-consteval, or
- the header synopsis begins with a comment that includes freestanding and the declaration is not followed by a comment that includes hosted.
[Example 2: // all freestanding namespace std { — end example][Note 3:Freestanding annotations follow some additional exposition conventions that do not impose any additional normative requirements.Header synopses that begin with a comment containing "all freestanding" contain no hosted items,andno freestanding deleted functions, no freestanding consteval functions, and no freestanding consteval classes.Header synopses that begin with a comment containing "mostly freestanding" contain at least one hosted item,orfreestanding deleted function, freestanding consteval function, or freestanding consteval class.Classes and class templates followed by a comment containing "partially freestanding" contain at least one hosted item,— end note]orfreestanding deleted function, or freestanding consteval function.
<string>
and <memory>
are already freestanding headers.Subclause | Header(s) | |
---|---|---|
[…] | […] | […] |
?.? [std.exceptions] | Exception classes | <stdexcept> |
[…] | […] | […] |
?.? [vector.syn] | Header <vector> synopsis | <vector> |
[…] | […] | […] |
Bump the constants on the following macros:#define __cpp_lib_freestanding_consteval_allocator 20XXXXL // freestanding, also in <memory> #define __cpp_lib_freestanding_consteval_string 20XXXXL // freestanding, also in <string> #define __cpp_lib_freestanding_stdexcept 20XXXXL // freestanding, also in <stdexcept> #define __cpp_lib_freestanding_vector 20XXXXL // freestanding, also in <vector>
#define __cpp_lib_freestanding_memory202502L20XXXXL // freestanding, also in <memory> /*...*/ #define __cpp_lib_freestanding_string_view202311L20XXXXL // freestanding, also in <string_view>
namespace std { class logic_error; // freestanding-consteval class domain_error; // freestanding-consteval class invalid_argument; // freestanding-consteval class length_error; // freestanding-consteval class out_of_range; // freestanding-consteval class runtime_error; // freestanding-consteval class range_error; // freestanding-consteval class overflow_error; // freestanding-consteval class underflow_error; // freestanding-consteval }
namespace std { class logic_error : public exception { public: constexpr explicit logic_error(const string& what_arg); // freestanding-consteval constexpr explicit logic_error(const char* what_arg); // freestanding-consteval }; }
namespace std { class domain_error : public logic_error { public: constexpr explicit domain_error(const string& what_arg); // freestanding-consteval constexpr explicit domain_error(const char* what_arg); // freestanding-consteval }; }
namespace std { class invalid_argument : public logic_error { public: constexpr explicit invalid_argument(const string& what_arg); // freestanding-consteval constexpr explicit invalid_argument(const char* what_arg); // freestanding-consteval }; }
namespace std { class length_error : public logic_error { public: constexpr explicit length_error(const string& what_arg); // freestanding-consteval constexpr explicit length_error(const char* what_arg); // freestanding-consteval }; }
namespace std { class out_of_range : public logic_error { public: constexpr explicit out_of_range(const string& what_arg); // freestanding-consteval constexpr explicit out_of_range(const char* what_arg); // freestanding-consteval }; }
namespace std { class runtime_error : public logic_error { public: constexpr explicit runtime_error(const string& what_arg); // freestanding-consteval constexpr explicit runtime_error(const char* what_arg); // freestanding-consteval }; }
namespace std { class range_error : public logic_error { public: constexpr explicit range_error(const string& what_arg); // freestanding-consteval constexpr explicit range_error(const char* what_arg); // freestanding-consteval }; }
namespace std { class overflow_error : public logic_error { public: constexpr explicit overflow_error(const string& what_arg); // freestanding-consteval constexpr explicit overflow_error(const char* what_arg); // freestanding-consteval }; }
namespace std { class underflow_error : public logic_error { public: constexpr explicit underflow_error(const string& what_arg); // freestanding-consteval constexpr explicit underflow_error(const char* what_arg); // freestanding-consteval }; }
// [default.allocator], the default allocator template<class T> class allocator; // partially freestanding template<class T, class U> constexpr bool operator==(const allocator<T>&, const allocator<U>&) noexcept; // freestanding
namespace std { template<class T> class allocator { public: using value_type = T; using size_type = size_t; using difference_type = ptrdiff_t; using propagate_on_container_move_assignment = true_type; constexpr allocator() noexcept; constexpr allocator(const allocator&) noexcept; template<class U> constexpr allocator(const allocator<U>&) noexcept; constexpr ~allocator(); constexpr allocator& operator=(const allocator&) = default; constexpr T* allocate(size_t n); // freestanding-consteval constexpr allocation_result<T*> allocate_at_least(size_t n); // freestanding-consteval constexpr void deallocate(T* p, size_t n); // freestanding-consteval }; }
// [string.view.access], element access constexpr const_reference operator[](size_type pos) const; constexpr const_reference at(size_type pos) const; // freestanding-deletedconsteval constexpr const_reference front() const; constexpr const_reference back() const; constexpr const_pointer data() const noexcept; // [string.view.modifiers], modifiers constexpr void remove_prefix(size_type n); constexpr void remove_suffix(size_type n); constexpr void swap(basic_string_view& s) noexcept; // [string.view.ops], string operations constexpr size_type copy(charT* s, size_type n, size_type pos = 0) const; // freestanding-deletedconsteval constexpr basic_string_view substr(size_type pos = 0, size_type n = npos) const; // freestanding-deletedconsteval constexpr int compare(basic_string_view s) const noexcept; constexpr int compare(size_type pos1, size_type n1, basic_string_view s) const; // freestanding-deletedconsteval constexpr int compare(size_type pos1, size_type n1, basic_string_view s, size_type pos2, size_type n2) const; // freestanding-deletedconsteval constexpr int compare(const charT* s) const; constexpr int compare(size_type pos1, size_type n1, const charT* s) const; // freestanding-deletedconsteval constexpr int compare(size_type pos1, size_type n1, const charT* s, size_type n2) const; // freestanding-deletedconsteval
// [basic.string], basic_string template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT>> class basic_string; // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, const basic_string<charT, traits, Allocator>& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, const basic_string<charT, traits, Allocator>& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, basic_string<charT, traits, Allocator>&& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, basic_string<charT, traits, Allocator>&& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const charT* lhs, const basic_string<charT, traits, Allocator>& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const charT* lhs, basic_string<charT, traits, Allocator>&& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(charT lhs, const basic_string<charT, traits, Allocator>& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(charT lhs, basic_string<charT, traits, Allocator>&& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, const charT* rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, const charT* rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, charT rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, charT rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, type_identity_t<basic_string_view<charT, traits>> rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, type_identity_t<basic_string_view<charT, traits>> rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(type_identity_t<basic_string_view<charT, traits>> lhs, const basic_string<charT, traits, Allocator>& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr basic_string<charT, traits, Allocator> operator+(type_identity_t<basic_string_view<charT, traits>> lhs, basic_string<charT, traits, Allocator>&& rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr bool operator==(const basic_string<charT, traits, Allocator>& lhs, const basic_string<charT, traits, Allocator>& rhs) noexcept; // freestanding-consteval template<class charT, class traits, class Allocator> constexpr bool operator==(const basic_string<charT, traits, Allocator>& lhs, const charT* rhs); // freestanding-consteval template<class charT, class traits, class Allocator> constexpr see below operator<=>(const basic_string<charT, traits, Allocator>& lhs, const basic_string<charT, traits, Allocator>& rhs) noexcept; // freestanding-consteval template<class charT, class traits, class Allocator> constexpr see below operator<=>(const basic_string<charT, traits, Allocator>& lhs, const charT* rhs); // freestanding-consteval // [string.special], swap template<class charT, class traits, class Allocator> constexpr void swap(basic_string<charT, traits, Allocator>& lhs, basic_string<charT, traits, Allocator>& rhs) noexcept(noexcept(lhs.swap(rhs))); // freestanding-consteval // [string.io], inserters and extractors template<class charT, class traits, class Allocator> basic_istream<charT, traits>& operator>>(basic_istream<charT, traits>& is, basic_string<charT, traits, Allocator>& str); template<class charT, class traits, class Allocator> basic_ostream<charT, traits>& operator<<(basic_ostream<charT, traits>& os, const basic_string<charT, traits, Allocator>& str); template<class charT, class traits, class Allocator> basic_istream<charT, traits>& getline(basic_istream<charT, traits>& is, basic_string<charT, traits, Allocator>& str, charT delim); template<class charT, class traits, class Allocator> basic_istream<charT, traits>& getline(basic_istream<charT, traits>&& is, basic_string<charT, traits, Allocator>& str, charT delim); template<class charT, class traits, class Allocator> basic_istream<charT, traits>& getline(basic_istream<charT, traits>& is, basic_string<charT, traits, Allocator>& str); template<class charT, class traits, class Allocator> basic_istream<charT, traits>& getline(basic_istream<charT, traits>&& is, basic_string<charT, traits, Allocator>& str); // [string.erasure], erasure template<class charT, class traits, class Allocator, class U = charT> constexpr typename basic_string<charT, traits, Allocator>::size_type erase(basic_string<charT, traits, Allocator>& c, const U& value); // freestanding-consteval template<class charT, class traits, class Allocator, class Predicate> constexpr typename basic_string<charT, traits, Allocator>::size_type erase_if(basic_string<charT, traits, Allocator>& c, Predicate pred); // freestanding-consteval // basic_string typedef-names using string = basic_string<char>; // freestanding using u8string = basic_string<char8_t>; // freestanding using u16string = basic_string<char16_t>; // freestanding using u32string = basic_string<char32_t>; // freestanding using wstring = basic_string<wchar_t>; // freestanding /* ... */ inline namespace literals { inline namespace string_literals { // [basic.string.literals], suffix for basic_string literals constexpr string operator""s(const char* str, size_t len); // freestanding-consteval constexpr u8string operator""s(const char8_t* str, size_t len); // freestanding-consteval constexpr u16string operator""s(const char16_t* str, size_t len); // freestanding-consteval constexpr u32string operator""s(const char32_t* str, size_t len); // freestanding-consteval constexpr wstring operator""s(const wchar_t* str, size_t len); // freestanding-consteval } }
Other than the destructor, all member functions and member function templates in the class templatebasic_string
are freestanding consteval functions ([freestanding.item]).
// [vector], class template vector template<class T, class Allocator = allocator<T>> class vector; // freestanding-consteval template<class T, class Allocator> constexpr bool operator==(const vector<T, Allocator>& x, const vector<T, Allocator>& y); // freestanding-consteval template<class T, class Allocator> constexpr synth-three-way-result<T> operator<=>(const vector<T, Allocator>& x, const vector<T, Allocator>& y); // freestanding-consteval template<class T, class Allocator> constexpr void swap(vector<T, Allocator>& x, vector<T, Allocator>& y) noexcept(noexcept(x.swap(y))); // freestanding-consteval // [vector.erasure], erasure template<class T, class Allocator, class U = T> constexpr typename vector<T, Allocator>::size_type erase(vector<T, Allocator>& c, const U& value); // freestanding-consteval template<class T, class Allocator, class Predicate> constexpr typename vector<T, Allocator>::size_type erase_if(vector<T, Allocator>& c, Predicate pred); // freestanding-consteval namespace pmr { template<class T> using vector = std::vector<T, polymorphic_allocator<T>>; } // [vector.bool], specialization of vector for bool // [vector.bool.pspc], partial class template specialization vector<bool, Allocator> template<class Allocator> class vector<bool, Allocator>; // freestanding-consteval
Other than the destructor, all member functions and member function templates in the class templatevector
are freestanding consteval functions ([freestanding.item]).
Other than the destructor, all member functions and member function templates invector<bool>
are freestanding consteval functions ([freestanding.item]).