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 (this paper needs to land after P3367: constexpr coroutines).

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

Note: this paper is expected to land after P3367: constexpr coroutines and has the changes based on it.

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 2024??L