Skip to content
Learn Netverks
6

Clang rejects constexpr construction through inherited CRTP constructors that GCC accepts

asked 6 hours ago by @qa-vit1lkgeqaizz0o8uxug 0 rep · 248 views

c++ clang language lawyer crtp

Godbolt Example

template<typename D>
struct Base {
    constexpr Base(int x) {
        static_cast<D*>(this)->value = x;
    }
};

struct Derived : public Base<Derived> {
    int value;
    using Base<Derived>::Base;
};

constexpr Derived d(42);

int main() {}

I have this fragment that doesn't compile on clang but does compile on mingw and msvc; can someone explain if this is standard-conforming and which compiler is right here?

I assume the problem is we write to the member of the derived class inside the base class constructor, which means we are writing data in an object that hasn't been constructed yet, which is apparently fine in runtime initialization, but is not fine in a constexpr context.

Comments on this question (0)

Use comments to ask for clarification — answers go in the answer box below.

Log in to comment on this question.

2 answers

9

Accepted answer

I assume the problem is we write to the member of the derived class inside the base class constructor, which means we are writing data in an object that hasn't been constructed yet

Correct.

which is apparently fine in runtime initialization, but is not fine in a constexpr context.

It's never fine, the behavior is undefined. You happened to get away with it.


[basic.life]/7.2

Before the lifetime of an object has started ... The program has undefined behavior if ...

— the pointer is used to access a non-static data member or call a non-static member function of the object, ...

This makes no exceptions for non-constexpr context.

Morgan Chen · 0 rep · 6 hours ago

5

[basic.life]/2 says that

The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • its initialization (if any) is complete (including vacuous initialization) ([dcl.init]),

except that ...

And the initialization of the non-static data member is not performed before the base class's constructor returns, because [class.base.init]/13 says

In a non-delegating constructor, initialization proceeds in the following order:

  • First, and only for the constructor of the most derived class ([intro.object]), virtual base classes are initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base classes in the derived class base-specifier-list.
  • Then, direct base classes are initialized in declaration order as they appear in the base-specifier-list (regardless of the order of the mem-initializers).
  • Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).
  • Finally, the compound-statement of the constructor body is executed.

So the lifetime of the data member value hasn't started yet when the write to it is executed in the constructor of Base.

[basic.life]/8 says that

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a glvalue refers to allocated storage ([basic.stc.dynamic.allocation]), and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if

  • (8.1) the glvalue is used to access the object, or ...

Therefore the write to static_cast<D*>(this)->value has undefined behavior, which is not fine. Moreover, since it is evaluated at compile-time, and according to [expr.const.core]/2:

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:

  • ...
  • (2.8) an operation that would have undefined or erroneous behavior as specified in [intro] through [cpp];
  • ...

the program is ill-formed since the compile-time evaluation involves a core language undefined behavior.

Clang is correct to diagnose this and reject the program.

Cameron Singh · 0 rep · 6 hours ago

Your answer