c++lambdalanguage-lawyerc++20default-arguments

At which point are default template arguments instantiated in C++?


Since every lambda expression in C++ has a unique closure type, and this type can be used as the default argument of a template parameter (since C++20, using decltype), it is important to know at which point that type is instantiated. Consider this C++20 example:

#include <concepts>

template<typename T = decltype([]{})>
using X = T;

template<typename T = X<>>
auto foo() { return T{}; }

int main() {
    using T1 = decltype(foo());
    using T2 = decltype(foo());
    static_assert(!std::same_as<T1, T2>);
}

Here, the default argument of the template parameter T in the definition of type alias X is the closure type decltype([]{}). The function template foo() has this as its return type.

If the instantiation of new closure type takes place every time foo() appears in the program, then the types T1 and T2 must be distinct, as the static_assertion passes. However, this assertion passes only when compiling with GCC.

For clang and MSVC, the types T1 and T2 are the same, meaning that instantiation of the default argument doesn't take place every time (but rather once, at the template declaration). See Compiler Explorer demo.

Which compiler is compliant with the standard?


Solution

  • The short answer is that this is an under-specified area of the standard. Default template arguments are simply assumed to have a "value", but it is unclear whether this is a single value, or whether each each use of a default-template argument is meant to produce a new instance of this value.

    While the standard is unclear on whether your assertion should pass or not, lambda expressions in default template arguments are an ODR footgun waiting to happen. The details are explained below.

    The Long Answer

    To dissect this, let's start at the call sites, namely the two occurrences of decltype(foo()). These are using function template argument deduction, so what happens is:

    When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the template are replaced with the corresponding deduced or default argument values.

    - [temp.deduct.general] §5

    This means that decltype(foo()) is translated into decltype(foo<X<>>()). This is the main point of ambiguity, because it is not stated whether "value" refers the type X<> or to the symbols comprising X<>. I.e., it is unclear whether the compiler is allowed to perform memoization like decltype(foo<__X_instantiation>()).

    Now we have to deal with X<>, which is a simple-template-id:

    When a simple-template-id does not name a function, a default template-argument is implicitly instantiated when the value of that default argument is needed.

    [Example 7:

    template<typename T, typename U = int> struct S { };
    S<bool>* p;         // the type of p is S<bool, int>*
    

    The default argument for U is instantiated to form the type S<bool, int>*. - end example]

    - [temp.arg.general] §9

    This means that we form the expression decltype(foo<X<decltype([]{})>>), where the template arguments of foo are possibly memoized. As for where []{} is declared:

    1. The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below.
    2. The closure type is declared in the smallest block scope, class scope, or namespace scope that contains the corresponding lambda-expression.

    - [expr.prim.lambda.closure] §1, §2

    If the compiler was allowed to perform memoization of X<>, then we would see the following code after:

    struct closure { /* ... */ };
    
    // The expression in decltype is not type-dependent, so decltype(closure{})
    // would NOT denote a new unique type.
    // The same applies to all uses of decltype in this problem.
    template<typename T = decltype(closure{})>
    using X = T;
    
    using __X_instantiation = X<decltype(closure{})>; // = closure
    
    template<typename T = __X_instantiation>
    auto foo() { return T{}; }
    
    int main() {
        using T1 = decltype(foo<decltype(__X_instantiation{})>()); // = closure
        using T2 = decltype(foo<decltype(__X_instantiation{})>()); // = closure
        /* ... */
    }
    
    template auto foo<__X_instantiation>() { return __X_instantiation{}; }
    

    Otherwise, we would end up with:

    /* ... */
    
    int main() {
        struct closure1 { /* ... */ };
        using T1 = decltype(foo<X<decltype(closure1{})>>()); // = closure1
        struct closure2 { /* ... */ };
        using T1 = decltype(foo<X<decltype(closure2{})>>()); // = closure2
        /* ... */
    }
    
    template auto foo<X<closure1>>() { return closure1{}; }
    template auto foo<X<closure2>>() { return closure2{}; }
    

    It is unclear which of these is correct, and depends on whether the implementation chooses memoization of X<>. This is implemented differently from compiler to compiler, which is why your static_assert(!std::same_as<T1, T2>) passes for some, but not for others.

    ODR-Violation Just Waiting To Happen

    Note that X is a massive footgun, because it is ill-formed if you include X in multiple translation units:

    template<typename T = decltype([]{})>
    using X = T;
    

    In [basic.def.odr], we see the disaster unfold:

    [...] a default template argument used by an (implicit or explicit) template-id or simple-template-id is treated as if its token sequence were present in the definition of [a template] D; that is, the default argument or default template argument is subject to the requirements described in this paragraph (recursively).

    - [basic.def.odr] §14.11

    This means, that when we use X<>, it is as if we wrote:

    template<typename T = __default_arg>
    using X = T, /* and declare */ __default_arg = decltype([]{});
    

    Note: obviously this is not legal C++, it's just meant to illustrate where the default template argument is considered to be for ODR purposes.

    If D is a template and is defined in more than one translation unit, [...] These requirements also apply to corresponding entities defined within each definition of D ([...]). For each such entity and for D itself, the behavior is as if there is a single entity with a single definition, including in the application of these requirements to other entities.

    - [basic.def.odr] §15

    Because the default argument is treated as if it was inside X, these ODR restrictions apply, and the behavior is as if there was one definition of decltype([]{}). However, because we declare a new closure type with each lambda expression, that is an ODR-violation just waiting to happen:

    /* [...] */
    inline void g(bool cond, void (*p)() = []{}) {
      if (cond) g(false);
    }
    /* [... ] */
    

    [...] If the definition of g appears in multiple translation units, the program is ill-formed (no diagnostic required) because each such definition uses a default argument that refers to a distinct lambda-expression closure type. [...]

    - [basic.def.odr] §16

    The above example equally applies to decltype([]{}) in template parameters, as the same rules in regards to the One-Definition Rule apply.

    Note: None of these rules help us disambiguate the question code, because they only apply if templates appear in multiple translation units.