Why does an inline user-provided constructor odr-uses base class constructors?

Viewed 109

Consider the following illustrative example

#include <iostream>

template <typename>
struct Base {
    static int const touch;
    Base() {
        (void)touch;
    }
};

template<typename CRTP>
int const Base<CRTP>::touch = []{
    std::cout << "Initialized!\n";
    return 0;
}();

struct A : Base<A> {
    A() {} 
};

struct B : Base<B> {
    B() = default;
};

int main() {
}

When the above program is compiled by GCC, Clang or VC++ and executed, one consistently sees the following output:

Initialized!

All three compilers emit the definition and initialization of Base<A>::touch, whereas neither emits the definition and initialization of Base<B>::touch (verified also via godbolt). So I would conclude this is standard sanctioned behavior.

For the defaulted constructor of B, we have

[class.ctor]

7 A default constructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used to create an object of its class type ([intro.object]) or when it is explicitly defaulted after its first declaration.

From which one may conclude that since neither condition applies in our TU, B::B() is never implicitly defined. So it never odr-uses Base<B>::Base() and Base<B>::touch. I find this reasonable.

However, I cannot understand why A::A() ends up odr-using the members of its base class. We know that

[class.mfct]

1 A member function may be defined in its class definition, in which case it is an inline member function ...

[dcl.inline]

6 An inline function or variable shall be defined in every translation unit in which it is odr-used and shall have exactly the same definition in every case ([basic.def.odr]).

[basic.def.odr]

4 ... A constructor for a class is odr-used as specified in [dcl.init].

We never initialize any objects of type A, so we shouldn't be odr-using its constructor. And so our program wouldn't end up containing any definition of A::A().

So why does it behave as though a definition exists? Why does it odr-use Base<A>::Base() and causes its instantiation?

1 Answers

This happens for any inline definition that odr-uses Base<T>::Base, for example:

inline void x() {
    Base<char>();
}

struct A : Base<A> {
    A(int) {}
    void x() {
        Base<int>();
    }
};

struct C : Base<C> {
    C();
};

inline C::C() = default;  // See [1]

// Will print "Initialized!" four times

B() = default does not odr-use Base<T>::Base because it is only defined as defaulted. Even though structurally a definition according to [basic.def]/2, it is not "emitted" because of [class.ctor]/7 (as you quoted). The definition that would odr-use Base<T>::Base will only be (implicitly) defined if and when B::B() is odr-used itself.

For other inline functions that are defined, there does not exist such an exemption. Their definitions unconditionally (and structurally) contain an odr-use Base<T>::Base. A::A() in your example is a definition, and your program does contain a definition of A::A() that uses Base<T>::Base.

"An inline function or variable shall be defined in every translation unit in which it is odr-used". This is a "shall" requirement (for every TU in which an inline function is odr-used, a definition is required), not the consequence of odr-using an inline function.


[1] As a consequence of [dcl.fct.def.default]/5:

A user-provided explicitly-defaulted function (i.e., explicitly defaulted after its first declaration) is defined at the point where it is explicitly defaulted

So C::C() has a default definition which immediately generates the "implicit" definition.

Related