Introduction

In modern C++, template metaprogramming often leads into deep corners of the C++ Standard, especially when dealing with default template arguments, non-type template parameters (NTTPs), and static variable template instantiations. A fascinating issue arises when taking the address of an initialized variable template (&inited_with_side_effect<>) inside a default template argument: Must the variable template be instantiated, and must its dynamic initializer execute?

Major compilers—GCC, Clang, and MSVC—disagree on this behavior, producing different results depending on whether the address is passed as an auto NTTP or wrapped inside a type parameter like std::integral_constant.

The Problem: Code Examples

Consider the following two minimal examples where a variable template with side effects in its dynamic initializer is used as a default template argument.

Example A: Address Encoded in a Type Argument

#include <cstdio>
#include <type_traits>

template <int = 0>
char inited_with_side_effect = (std::puts("INIT"), 0);

template <typename = std::integral_constant<char*, &inited_with_side_effect<>>>
constexpr int v = 0;

int main() {
    return v<>;
}

Example B: Address as an auto Non-Type Template Parameter

#include <cstdio>

template <int = 0>
char inited_with_side_effect = (std::puts("INIT"), 0);

template <auto = &inited_with_side_effect<>>
constexpr int v = 0;

int main() {
    return v<>;
}

Observed Compiler Behavior

Running these programs across the latest versions of standard compilers reveals surprising inconsistencies:

  • Clang: Prints INIT for both Example A and Example B.
  • GCC: Does not print for Example A, but prints INIT for Example B.
  • MSVC: Prints INIT for Example A, but does not print for Example B.

What Does the C++ Standard Say?

To determine if this is a compiler bug or conforming behavior, we must trace through several sections of the C++ Standard:

1. Is the Default Argument Instantiated?

Per [temp.arg.general], when v<> is referenced in main(), the default template argument is instantiated to determine the specialization of v. Therefore, the expression &inited_with_side_effect<0> is evaluated at template instantiation time.

2. ODR-Use and Implicit Instantiation

According to [basic.def.odr], a variable is odr-used if it is named by a potentially evaluated expression, unless an exception applies. Taking the address of a non-const variable (using the unary & operator) makes the variable odr-used because its address is required as a constant address at compile time.

Furthermore, under [temp.inst], a variable template specialization is implicitly instantiated when it is referenced in a context that requires a variable definition to exist (such as an odr-use). Once instantiated, inited_with_side_effect<0> becomes a non-inline variable with static storage duration.

3. Dynamic Initialization Requirements

According to [basic.start.dynamic]:

"It is implementation-defined whether the dynamic initialization of a non-block non-inline variable with static storage duration is sequenced before the first statement of main or is deferred."

However, if the initialization is deferred, the standard specifies that it must strongly happen before any non-initialization odr-use of any non-inline function or variable defined in the same translation unit (TU). Because main() is defined in the same TU and called, the dynamic initialization of inited_with_side_effect<0> is required to execute before or during the execution of main().

Are These Compiler Bugs?

Yes, both GCC and MSVC exhibit conformance bugs here:

  • GCC's Bug (Example A): GCC fails to register the odr-use / instantiation of the template variable when the address expression appears as a template argument inside a type template (std::integral_constant).
  • MSVC's Bug (Example B): MSVC treats the address in the auto NTTP default argument purely as a symbolic address constant without properly triggering the emission and registration of the static variable's dynamic initializer.
  • Clang: Clang strictly adheres to the standard in both scenarios by consistently instantiating the variable template and emitting its dynamic initialization.

What if v<> is Never Used?

If v<> is never referenced in the program (e.g., main() is empty), default template arguments for unused templates are not instantiated. As a consequence:

  • inited_with_side_effect<0> is never implicitly instantiated.
  • No definition exists in the translation unit.
  • The side effect (std::puts("INIT")) is guaranteed not to occur.

Key Takeaways & Best Practices

  • Avoid Dynamic Side Effects in Template Arguments: Relying on dynamic initializers of variable templates triggered via default template arguments is fragile due to inconsistent compiler implementations.
  • Prefer constexpr / consteval Initialization: If you need compile-time registration or static checks, use constexpr functions or structural types instead of runtime side effects during static initialization.
  • Clang is Currently the Most Compliant: If your build depends on strict ODR-use triggering of variable template instantiations via default arguments, Clang offers the most standard-conforming behavior.