c++language-lawyerc++20

Are repeated declarations of a template with a type-template-parameter defaulted to a closure type a violation of ODR?


Since C++20 gave us lambdas in unevaluated context, I can write the following code.

template <class = decltype([]{})>
class MyType {};

If I were to include this definition in multiple TUs, would it be an ODR violation?

I'm looking in particular at §6.3(13.13) of N4860:

In each such definition, [...] 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 D; that is, the default argument or default template argument is subject to the requirements described in this paragraph (recursively).

which I read as, decltype([]{}) is part of the declared type, which is then an example explicitly labeled as invalid by the original paper (§4). Am I reading this correctly?

Edit 1: here's another quote from the same paragraph of the standard which I find very confusing:

There can be more than one definition of a
[...]
(13.4) — templated entity (13.1),
[...]
(13.6) — default template argument
in a program provided that each definition appears in a different translation unit and the definitions satisfy the following requirements. Given such an entity D defined in more than one translation unit, for all definitions of D, [...] the following requirements shall be satisfied.
[...]
(13.10) — In each such definition, except within the default arguments and default template arguments of D, corresponding lambda-expressions shall have the same closure type (see below).

Now, we can apply this paragraph to my code snippet above either
a) by taking D to be the class template (which is a templated entity), in which case (13.10) seems to say the default template argument is exempt from this rule, or
b) by taking D to be the default template argument, to which this rule must be applied. The "see below" at the end of the quote refers to, I presume, point 14:

If D is a template and is defined in more than one translation unit, then the preceding requirements shall apply both to names from the template’s enclosing scope used in the template definition (13.8.3), and also to dependent names at the point of instantiation (13.8.2). These requirements also apply to corresponding entities defined within each definition of D (including the closure types of lambda-expressions, but excluding entities defined within default arguments or default template arguments of either D or an entity not defined within 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. [Note: The entity is still declared in multiple translation units, and 6.6 still applies to these declarations. In particular, lambda-expressions (7.5.5) appearing in the type of D may result in the different declarations having distinct types, and lambda-expressions appearing in a default argument of D may still denote different types in different translation units. — end note]

from which I understand: if D is my template class, the default template argument is exempt from this rule. But then the note at the end goes on to specify that the lambda-expression in the default template argument might have different types in different TUs, hence, I presume, breaking ODR.

Edit 2: very much related, but I can't seem to get to a final conclusion still:
Meaning of note about multiply defined items
Do lambda expressions that appear in different definitions of a same entity produce the same closure type?


Solution

  • Yes, you would get an ODR violation in this case, and it's usually IFNDR except in the named module case mentioned in [basic.def.odr]/15.

    [basic.def.odr]/1.7 states that default template arguments are one of the kinds of definable item and [basic.def.odr]/15 requires multiple definitions of a definable item to be "the same". However, every lambda expression produces a distinct type, including the same lambda spelled the same way in two different translation units. There are situations in which a lambda has the same type across two TUs because it is part of an enclosing definition:

    inline int foo() {
        return []{ return 0; }();  // same lambda in every TU
    }
    

    This is only because [basic.def.odr]/17 allows multiple definitions of a definable item to behave as if there were a single definition in the program. In the case of foo, a single definition across all TUs implies a single lambda closure type across all TUs.

    Does this unification apply to the OP's class template? No, because [basic.def.odr]/17 has a carve-out for default arguments and default template arguments:

    These requirements also apply to corresponding entities defined within each definition of D (including the closure types of lambda-expressions, but excluding entities defined within default arguments or default template arguments of either D or an entity not defined within 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.

    Taking D as the class template MyType, we see that the default template argument for its (unnamed) template parameter is an excluded entity, so the behavior is not as if there is only a single definition of the default argument across the entire program.

    On the other hand, taking D to be the default argument definition itself, we see that [basic.def.odr]/15 is violated because bullet 15.6 requires:

    In each such definition, except within the default arguments and default template arguments of D, corresponding lambda-expressions shall have the same closure type (see below).

    Because D in this case is the default template argument itself, it doesn't have any of its own default template arguments (D is after all not itself a template) so it can't benefit from the exception in this bullet. The lambda-expression must be the same in all definitions. Because /15 is violated, D cannot benefit from /17, it's just an ODR violation.

    One of the examples in p18 uses a default function argument instead of a default template argument:

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

    The explanatory text states that defining g in multiple TUs causes an ODR violation because each lambda has a different type. Since default function arguments are treated the same way as default template arguments for the purposes of the ODR, the OP's example was clearly intended to also be an ODR violation.