constexpr virtual inheritance

This paper proposes allowing usage of virtual inheritance. This will allow in future constexpr-ification of std::ios_base, and streams, removing final limitation for making most of stream formatting constexpr.

This will also remove definition of constexpr-suitable as it will be meaningless and clean all references across draft of the standard (if this paper land after P3367: constexpr coroutines).

Changes

Motivation

Last language syntax thing which is disallowed in [dcl.constexpr] which blocks us from making std::stringstream constant evaluatable, which blocks us from making exception types for <chrono> (chrono::nonexistent_local_time and chrono::ambiguous_local_time). Stream formatting <chrono>. Using basic_istream_view in range code for compile-time parsing.

When we remove this limitations, language will only have limit on evaluation properties of code in [expr.const], and not syntactical (with exception of reinterpret_cast, but this is also defined as not constant evaluatable.)

Then keyword constexpr can be seen only as an opt-in into constant evaluatable and can be later removed and replaced with an opt-out attribute (this is not propesed here.)

Example of allowed code

struct Superbase {
	string id{"name"};
};

struct Common: Superbase {
	unsigned counter{0};
};

struct Left: virtual Common {
	unsigned value{0};
	constexpr const unsigned & get_counter() const {
		return Common::counter;
	}
};

struct Right: virtual Common {
	unsigned value{0};
	constexpr const unsigned & get_counter() const {
		return Common::counter;
	}
};

struct Child: Left, Right {
	unsigned x{0};
	unsigned y{0};
	// ...
};

constexpr auto ch = Child{}; // before: not allowed to even construct
                             // after: works as expected

static_assert(&ch.Left::get_counter() == &ch.Right::get_counter());

Hierarchy

counter value value y x name Child Right Left Common Superbase

Clang's representation

instance of Child bases virtualbases members members members members bases members subobject Left subobject Right subobject Common subobject Superbase x value value y counter name

Implementation experience

In progress in clang's fork. Currently structures are represented as APValue object with type field struct and pointer to an array of APValue-s containing n APValue-s representing base types and k APValue representing each member subobject.

Lookup for outermost object is already implemented in clang in order to implement virtual function calls. So only changes needed are:

  • Allow functions to be constexpr if they are within type with virtual bases (in SemaDeclCXX.cpp, note: clang currently disallows not just constructor/destructor but all member functions to be constexpr)
  • Allow default constructors for types with virtual bases (in SemaType.cpp)
  • Allow computing dynamic type for objects (in ExprConstant.cpp)
  • Extend APValue to contain number of virtual bases (in APValue.h)
  • Make construction of outermost object contain subobject representing virtual bases (in ExprConstant.cpp)
  • Handle destruction of outermost object to properly destroy also virtual bases (in ExprConstant.cpp)
  • Handle zero-initialization of outermost object (in ExprConstant.cpp)

I spoke with other implementors and they don't seem to have any major concern about implementability.

Library impact

Removing last limitation on constexpr-suitable as defined in [dcl.constexpr] will make it a tautology as every function will now be constexpr-suitable hence this paper contains changes to removal of it across wording.

But this term was used somehow badly to make specify what must be constant evaluatable. It's a subject of LWG issue 2833. Library needs to invent wording specifying something like "implementation must make sure this functionality is constant evaluatable by avoiding constructs disallowed in [expr.const]". As constexpr keyword doesn't mean something must be constant-evaluatable, it's just an opt-in into evaluation for some (or none) code-paths.

I still kept removal of all references to constexpr-suitable in this paper for the project editor's convenience but I guess LWG will do some changes there as they will resolve the issue 2833.

Proposed changes to wording

Paper contains two sets of wordings. One is based on current draft and one is based on changes in P3367 constexpr coroutines.

Based on current draft

9.2.6 The constexpr and consteval specifiers [dcl.constexpr]

A constexpr or consteval specifier used in the declaration of a function declares that function to be a constexpr function.
[Note 3: 
A function or constructor declared with the consteval specifier is an immediate function ([expr.const]).
— end note]
A destructor, an allocation function, or a deallocation function shall not be declared with the consteval specifier.
A function is constexpr-suitable if  

Based on P3367 constexpr coroutines

9.2.6 The constexpr and consteval specifiers [dcl.constexpr]

A constexpr or consteval specifier used in the declaration of a function declares that function to be a constexpr function.
[Note 3: 
A function or constructor declared with the consteval specifier is an immediate function ([expr.const]).
— end note]
A destructor, an allocation function, or a deallocation function shall not be declared with the consteval specifier.
A function is constexpr-suitable unless it is a constructor or destructor whose class has any virtual base classes.
Except for instantiated constexpr functions, non-templated constexpr functions shall be constexpr-suitable.
[Example 2: constexpr int square(int x) { return x * x; } // OK constexpr long long_max() { return 2147483647; } // OK constexpr int abs(int x) { if (x < 0) x = -x; return x; // OK } constexpr int constant_non_42(int n) { // OK if (n == 42) { static int value = n; return value; } return n; } constexpr int uninit() { struct { int a; } s; return s.a; // error: uninitialized read of s.a } constexpr int prev(int x) { return --x; } // OK constexpr int g(int x, int n) { // OK int r = 1; while (--n > 0) r *= x; return r; } — end example]
An invocation of a constexpr function in a given context produces the same result as an invocation of an equivalent non-constexpr function in the same context in all respects except that
[Note 4: 
Declaring a function constexpr can change whether an expression is a constant expression.
This can indirectly cause calls to std​::​is_constant_evaluated within an invocation of the function to produce a different value.
— end note]
[Note 5: 
It is possible to write a constexpr function for which no invocation satisfies the requirements of a core constant expression.
— end note]

Dependent changes in wording

Note: all following changes removes reference to now non-existing term constexpr-suitable. If it was a requirement for making function marked constexpr, this makes them constexpr unconditionaly. I'm considering these changes editorial and not changing any meaning.

7.5.6.2 Closure types [expr.prim.lambda.closure]

The function call operator or operator template is a static member function or static member function template ([class.static.mfct]) if the lambda-expression's parameter-declaration-clause is followed by static.
Otherwise, it is a non-static member function or member function template ([class.mfct.non.static]) that is declared const ([class.mfct.non.static]) if and only if the lambda-expression's parameter-declaration-clause is not followed by mutable and the lambda-declarator does not contain an explicit object parameter.
It is neither virtual nor declared volatile.
Any noexcept-specifier specified on a lambda-expression applies to the corresponding function call operator or operator template.
An attribute-specifier-seq in a lambda-declarator appertains to the type of the corresponding function call operator or operator template.
An attribute-specifier-seq in a lambda-expression preceding a lambda-declarator appertains to the corresponding function call operator or operator template.
The function call operator or any given operator template specialization is a constexpr function if either the corresponding lambda-expression's parameter-declaration-clause is followed by constexpr or consteval, or it is constexpr-suitable ([dcl.constexpr]).
It is an immediate function ([dcl.constexpr]) if the corresponding lambda-expression's parameter-declaration-clause is followed by consteval.
[Example 3: auto ID = [](auto a) { return a; }; static_assert(ID(3) == 3); // OK struct NonLiteral { NonLiteral(int n) : n(n) { } int n; }; static_assert(ID(NonLiteral{3}).n == 3); // error — end example]
[Example 4: auto monoid = [](auto v) { return [=] { return v; }; }; auto add = [](auto m1) constexpr { auto ret = m1(); return [=](auto m2) mutable { auto m1val = m1(); auto plus = [=](auto m2val) mutable constexpr { return m1val += m2val; }; ret = plus(m2()); return monoid(ret); }; }; constexpr auto zero = monoid(0); constexpr auto one = monoid(1); static_assert(add(one)(zero)() == one()); // OK // Since two below is not declared constexpr, an evaluation of its constexpr member function call operator // cannot perform an lvalue-to-rvalue conversion on one of its subobjects (that represents its capture) // in a constant expression. auto two = monoid(2); assert(two() == 2); // OK, not a constant expression. static_assert(add(one)(one)() == two()); // error: two() is not a constant expression static_assert(add(one)(one)() == monoid(2)()); // OK — end example]
[Note 3: 
The function call operator or operator template can be constrained ([temp.constr.decl]) by a type-constraint ([temp.param]), a requires-clause ([temp.pre]), or a trailing requires-clause ([dcl.decl]).
[Example 5: template <typename T> concept C1 = /* ... */; template <std::size_t N> concept C2 = /* ... */; template <typename A, typename B> concept C3 = /* ... */; auto f = []<typename T1, C1 T2> requires C2<sizeof(T1) + sizeof(T2)> (T1 a1, T1 b1, T2 a2, auto a3, auto a4) requires C3<decltype(a4), T2> { // T2 is constrained by a type-constraint. // T1 and T2 are constrained by a requires-clause, and // T2 and the type of a4 are constrained by a trailing requires-clause. }; — end example]
— end note]

7.7 Constant expressions [expr.const]

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:
  • an invocation of an undefined constexpr function;
  • an invocation of an instantiated constexpr function that is not constexpr-suitable;
  • an invocation of a virtual function ([class.virtual]) for an object whose dynamic type is constexpr-unknown;

9.5.2 Explicitly-defaulted functions [dcl.fct.def.default]

A function explicitly defaulted on its first declaration is implicitly inline ([dcl.inline]), and is implicitly constexpr ([dcl.constexpr]) if it is constexpr-suitable.
[Note 1: 
Other defaulted functions are not implicitly constexpr.
— end note]

11.4.5.2 Default constructors [class.default.ctor]

An implicitly-defined ([dcl.fct.def.default]) default constructor performs the set of initializations of the class that would be performed by a user-written default constructor for that class with no ctor-initializer ([class.base.init]) and an empty compound-statement.
If that user-written default constructor would be ill-formed, the program is ill-formed.
If that user-written default constructor would be constexpr-suitable ([dcl.constexpr]), theAn implicitly-defined default constructor is constexpr.
Before the defaulted default constructor for a class is implicitly defined, all the non-user-provided default constructors for its base classes and its non-static data members are implicitly defined.
[Note 1: 
An implicitly-declared default constructor has an exception specification ([except.spec]).
An explicitly-defaulted definition might have an implicit exception specification, see [dcl.fct.def].
— end note]
[Note 2: 
A default constructor is implicitly invoked to initialize a class object when no initializer is specified ([dcl.init.general]).
Such a default constructor needs to be accessible ([class.access]).
— end note]
[Note 3: 
[class.base.init] describes the order in which constructors for base classes and non-static data members are called and describes how arguments can be specified for the calls to these constructors.
— end note]

11.4.5.3 Copy/move constructors [class.copy.ctor]

If an implicitly-defined ([dcl.fct.def.default]) constructor would be constexpr-suitable ([dcl.constexpr]), theAn implicitly-defined constructor is constexpr.
Before the defaulted copy/move constructor for a class is implicitly defined, all non-user-provided copy/move constructors for its potentially constructed subobjects are implicitly defined.
[Note 6: 
An implicitly-declared copy/move constructor has an implied exception specification ([except.spec]).
— end note]

11.4.7 Destructors [class.dtor]

A defaulted destructor is a constexpr destructor if it is constexpr-suitable ([dcl.constexpr]).
Before a defaulted destructor for a class is implicitly defined, all the non-user-provided destructors for its base classes and its non-static data members are implicitly defined.

22.3.2 Class template pair [pairs.pair]

Constructors and member functions of pair do not throw exceptions unless one of the element-wise operations specified to be called for that operation throws an exception.
The defaulted move and copy constructor, respectively, of pair is a constexpr function if and only if all required element-wise initializations for move and copy, respectively, would be constexpr-suitable ([dcl.constexpr]).
If (is_trivially_destructible_v<T1> && is_trivially_destructible_v<T2>) is true, then the destructor of pair is trivial.

22.4.4.2 Construction [tuple.cnstr]

The defaulted move and copy constructor, respectively, of tuple is a constexpr function if and only if all required element-wise initializations for move and copy, respectively, would be constexpr-suitable ([dcl.constexpr]).
The defaulted move and copy constructor of tuple<> are constexpr functions.
If is_trivially_destructible_v<> is true for all , then the destructor of tuple is trivial.

22.6.3.2 Constructors [variant.ctor]

In the descriptions that follow, let i be in the range [0, sizeof...(Types)), and be the type in Types.
constexpr variant() noexcept(see below);
Constraints: is_default_constructible_v<> is true.
Effects: Constructs a variant holding a value-initialized value of type .
Postconditions: valueless_by_exception() is false and index() is 0.
Throws: Any exception thrown by the value-initialization of .
Remarks: This function is constexpr if and only if the value-initialization of the alternative type would be constexpr-suitable ([dcl.constexpr]).
The exception specification is equivalent to is_nothrow_default_constructible_v<>.
[Note 1: 
See also class monostate.
— end note]

30.5.1 General [time.duration.general]

Members of duration do not throw exceptions other than those thrown by the indicated operations on their representations.
The defaulted copy constructor of duration shall be a constexpr function if and only if the required initialization of the member rep_ for copy and move, respectively, would be constexpr-suitable ([dcl.constexpr]).
[Example 1: duration<long, ratio<60>> d0; // holds a count of minutes using a long duration<long long, milli> d1; // holds a count of milliseconds using a long long duration<double, ratio<1, 30>> d2; // holds a count with a tick period of of a second // (30 Hz) using a double — end example]

Feature test macro

15.11 Predefined macro names [cpp.predefined]

__cpp_constexpr_virtual_inheritance 2025??L