Question

If a lambda is declared inside a default argument, is it different for each call site?

#include <iostream>

int foo(int x = [](){ static int x = 0; return ++x; }()) {
    return x;
};

int main() {
    std::cout << foo() << foo(); // prints "12", not "11"
}

I know that default arguments are evaluated each time a function is called. Does that mean that the lambda type is different on each call? Please point to the standard quotes explaining the behaviour here.

 35  3526  35
1 Jan 1970

Solution

 21

This example from dcl.fct.default makes pretty clear that the intent is that the point where the default argument is defined also defines the semantics:

int a = 1;
int f(int);
int g(int x = f(a));            // default argument: f(​::​a)

void h() {
  a = 2;
  {
    int a = 3;
    g();                        // g(f(​::​a))
  }
}

In particular, the default argument is not just a token sequence that is inserted at the point of the function call and then analysed.

Following this intent, the lambda expression is analysed at the point of definition of the default argument, not at the point of function call. Therefore, there is only one lambda type, not many, and the correct result is 12.

The Standard doesn't express this clearly enough with regards to lambda expressions being used as default arguments, though.

2024-07-01

Solution

 13

This is CWG 2300 and the behavior of the program can be explained using basic.def.odr#15.6 and basic.def.odr that implies the output 12 to be correct.

From basic.def.odr#15.6:

  1. For any definable item D with definitions in multiple translation units,
  • 15.1. ...
  • 15.2. ...

the program is ill-formed; a diagnostic is required only if the definable item is attached to a named module and a prior definition is reachable at the point where a later definition occurs. Given such an item, for all definitions of D, or, if D is an unnamed enumeration, for all definitions of D that are reachable at any given program point, the following requirements shall be satisfied.

15.3. ...

15.4. ...

15.5. ...

15.6. 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).

Note the "exception" given above for default arguments of D. More importantly, note that the exception don't apply to your example because in your example foo is a non-inline non-template function which means it can only ever be defined in only translation unit only. Which in turn means that there is only one definition of the lambda itself and hence only one unique closure type exist in your example.


This can further be seen from basic.def.odr:

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, and also to dependent names at the point of instantiation ([temp.dep]). 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 4: The entity is still declared in multiple translation units, and [basic.link] still applies to these declarations. In particular, lambda-expressions ([expr.prim.lambda]) appearing in the type of D can result in the different declarations having distinct types, and lambda-expressions appearing in a default argument of D might still denote different types in different translation units. — end note]

[Example 6:

inline void f(bool cond, void (*p)()) {
 if (cond) f(false, []{});
}
inline void g(bool cond, void (*p)() = []{}) {
 if (cond) g(false);
}
struct X {
 void h(bool cond, void (*p)() = []{}) {
   if (cond) h(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. The definition of X can appear in multiple translation units of a valid program; the lambda-expressions defined within the default argument of X​::​h within the definition of X denote the same closure type in each translation unit. — end example]

From here also we come to the same conclusion that in your example the definition of function foo appears only in one translation unit. The given program is well-formed and there is only one closure-type in your given example(in a single TU) and so the output 12 is correct as per the current wording.

2024-07-01