Search code examples
c++templateslanguage-lawyer

Is instantiation required for an unused but initialized const int static data member of a class template?


If a class template is instantiated, and it contains an initialized const int static data member that is unused, must that data member (and hence its initializer) be instantiated?

Example:

template <typename T>
struct S {
  static void m() {}
  static const int c = sizeof(T);
};

struct A;

int main()
{
  S<A>::m();
  return 0;
}

There is no use of c in the program, so at first glance one might think it is not instantiated, but Clang-17 and MSVC-19 do appear to instantiate it, and consequently reject due to A being incomplete. Meanwhile, GCC-13 accepts: https://godbolt.org/z/GEcfe8a6M

For convenience, the specific error from Clang is:

<source>:4:24: error: invalid application of 'sizeof' to an incomplete type 'A'
    4 |   static const int c = sizeof(T);
      |                        ^~~~~~~~~
<source>:11:3: note: in instantiation of template class 'S<A>' requested here
   11 |   S<A>::m();
      |   ^
<source>:7:8: note: forward declaration of 'A'
    7 | struct A;
      |        ^

but, for clarity, my primary question is whether the original example is valid C++.

In [temp.inst] of the C++ 17 standard, paragraph 3 says:

Unless a member of a class template or a member template has been explicitly instantiated or explicitly specialized, the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist or if the existence of the definition of the member affects the semantics of the program; in particular, the initialization (and any associated side effects) of a static data member does not occur unless the static data member is itself used in a way that requires the definition of the static data member to exist.

I find the wording of the first part confusing, since it seems circular. It says the member is instantiated if its existence affects the program semantics. But if its existence did not affect semantics, then whether it was instantiated or not would be unobservable! Thus, it would be equivalent to say that all members are instantiated, but that is of course not what implementations do. (Note: I'm assuming that "instantiate a member" means "instantiate its definition", in line with the conclusion at Are static members of a class template implicitly instantiated, or not?.)

Then, the second part, beginning with "in particular", seems to contradict the first part. The second part says instantiation of a static data member only happens if the member is used, but of course usage is just one way its existence could affect semantics. It also seems to imply that, in the example above, c should not be instantiated due to lack of usage.

In the same section, paragraph 7 begins:

The existence of a definition of a variable or function is considered to affect the semantics of the program if the variable or function is needed for constant evaluation by an expression (8.6), [...]

but that does not seem to apply to this example because c is never needed for constant evaluation (again, it is not used at all).

[class.static.data] paragraph 3 begins:

If a non-volatile non-inline const static data member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression (8.6). [...]

I think this rule should only apply if the member is instantiated since the result is dependent on the template argument. However, I suspect that rule is involved because if I move the member definition outside the class body, then all of the compilers accept it: https://godbolt.org/z/9cG1nzvnq

Based on reading the standard, I would conclude that S<A>::c should not be instantiated. GCC agrees, but Clang and MSVC disagree. Which is right?


Solution

  • tldr; The program is ill-formed because static const int c = sizeof(T);(including the initializer) is a declaration and is instantiated with the implicit instantiation of the class template specialization S<A> which results in the instantiation of sizeof(T) but since T is incomplete at that point, the program is ill-formed.


    From temp.inst#3:

    The implicit instantiation of a class template specialization causes:

    • the implicit instantiation of the declarations, but not of the definitions, of the non-deleted class member functions, member classes, scoped member enumerations, static data members, member templates, and friends; and

    (emphasis mine)

    This means that the implicit instantiation S<A> will cause the implicit instantiation of only the declaration of the static data member c but not the "definition". And since here the declaration includes the initializer sizeof(T), it will also be instantiated making the program ill-formed.

    Now from basic.def combined with class.static.data, we can conclude that static const int c = sizeof(T); is declaration but not a definition:

    Each entity declared by a declaration is also defined by that declaration unless:

    • it declares a non-inline static data member in a class definition ([class.mem], [class.static]),

    Further from class.static.data:

    The declaration of a non-inline static data member in its class definition is not a definition and may be of an incomplete type other than cv void.

    This means that static const int c = sizeof(T); is only a declaration but not a definition.


    Note that for integral and enumeration type declaration can have initializer as per class.static.data:

    If a non-volatile non-inline const static data member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression ([expr.const]). The member shall still be defined in a namespace scope if it is odr-used ([basic.def.odr]) in the program and the namespace scope definition shall not contain an initializer. The declaration of an inline static data member (which is a definition) may specify a brace-or-equal-initializer. If the member is declared with the constexpr specifier, it may be redeclared in namespace scope with no initializer (this usage is deprecated; see [depr.static.constexpr]). Declarations of other static data members shall not specify a brace-or-equal-initializer.