Does `using` in a template class force instantiation?

114 views Asked by At

This question is on how to use global variable initialization to achieve some side effect before main() is run (e.g., factory class registration before main()). This can be tricky when template is involved. Consider the following piece of code:

template <class T>
class A {
  public:
    static bool flag;
    static bool do_something() {
        // Side effect: do something here.
    }

    template <bool&>
    struct Dummy {};

    // Important: Do not delete.
    // This line forces the instantiation and initialization of `flag`.
    using DummyType = Dummy<flag>;
};

template <class T>
bool A<T>::flag = A<T>::do_something();

// A<int>::flag is instantiated and.
// A<int>::do_something will be called before main to initialize A<int>::flag.
class B : A<int> {};

If we remove the using, the behavior will be different:

template <class T>
class A {
  public:
    static bool flag;
    static bool do_something() {
        // Side effect: do something here.
    }
  // The `using` dummy part is missing.
};

template <class T>
bool A<T>::flag = A<T>::do_something();

// A<int>::flag is not instantiated.
// A<int>::do_something will not be called.
class B : A<int> {};

My question: is the using trick here a compiler specific thing, or is it backed up by the C++ standard? I did some search and found some relevant information about the standard 14.7.1, but still not sure about the rules on using as it's not mentioned in 14.7.1 (my source is this one, not sure if it should be considered as the ground truth). Also, the using Dummy seems somewhat hacky to me. Are there any other workarounds to make it more elegant? The goal is to resolve the instantiation within class A itself so that the side effect related code segments remain private.

1

There are 1 answers

0
Brian Bi On

Thanks to new wording in C++20, it is possible to give a definitive answer to your question. In the comments, Igor Tandetnik mentioned the relevant paragraph in C++17, namely [temp.inst]/3. In C++20, it is [temp.inst]/4, which has some new relevant wording that I have bolded:

Unless a member of a class template or a member template is a declared specialization, 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.

Later in the same section, paragraph 8:

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 (7.7), even if constant evaluation of the expression is not required or if constant expression evaluation does not use the definition.

In short, if the alias-declaration using DummyType = Dummy<flag>; is instantiated, then the static data member flag is needed for constant evaluation, therefore it is instantiated.

Informally, "needed for constant evaluation" refers to certain situations where a variable or function is not odr-used, but the way it is used in constant evaluation still requires its definition.

Formally, the definition of "needed for constant evaluation" is given in [expr.const]/15:

An expression or conversion is potentially constant evaluated if it is:

  • a manifestly constant-evaluated expression,
  • a potentially-evaluated expression (6.3),
  • an immediate subexpression of a braced-init-list,
  • an expression of the form & cast-expression that occurs within a templated entity, or
  • a subexpression of one of the above that is not a subexpression of a nested unevaluated operand.

A function or variable is needed for constant evaluation if it is:

  • a constexpr function that is named by an expression (6.3) that is potentially constant evaluated, or
  • a variable whose name appears as a potentially constant evaluated expression that is either a constexpr variable or is of non-volatile const-qualified integral type or of reference type.

"Manifestly constant-evaluated" is defined in p14 of that section:

An expression or conversion is manifestly constant-evaluated if it is: [...] a constant-expression, or [...] [Note 7: A manifestly constant-evaluated expression is evaluated even in an unevaluated operand. — end note]

"constant-expression" is a grammar production that is used in various contexts to denote a conditional-expression that must be a constant expression. We can check the grammar for simple-template-id (which is what Dummy<flag> is):

simple-template-id :
     template-name < template-argument-list(opt) >

[...]

template-argument-list :
     template-argument ...(opt)
     template-argument-list , template-argument ...(opt)

template-argument :
     constant-expression
     type-id
     id-expression

Here, id-expression refers to a template template parameter. In using DummyType = Dummy<flag>;, flag is an argument for a non-type template parameter, so it is a constant-expression.

Since the expression flag is a manifestly constant-evaluated expression, it is a potentially constant-evaluated expression. The variable flag's name "appears as a potentially constant evaluated expression" and it's a variable of reference type, so it is needed for constant evaluation.

Note that the concept of "needed for constant evaluation" was introduced by P0589 to resolve CWG 1581. This was voted as a DR, so its resolution is retroactive. The concepts of "needed for constant evaluation" and "manifestly constant-evaluated" always needed to be in C++; their absence led to issues like the one in this question.