35
#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.

20

6 Answers 6

20

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.

3
  • So, you are referring to the sentence "The names in the default argument are looked up, and the semantic constraints are checked, at the point where the default argument appears (...)"? If so, to the part about name lookup (meaning that operator() is only looked up once) or the part about semantic constraints (what does it mean)? The example is good and all, but usually they are non-normative.
    – cppbest
    Commented Jul 2 at 4:27
  • @cppbest I don't refer to any normative text, because I didn't find anything that would talk about lambda expressions specifically. The example just captures the spirit ("intent"): it matters what we have at the location of the default argument definition. The call sites then just use what was defined. In the case of a lambda expression we have just one definition, and that one is used at the call sites.
    – j6t
    Commented Jul 2 at 5:46
  • Unless this is in a header file consumed by multiple translation units, in which case each may produce a distinct type, which the linker may or may not merge.
    – Miral
    Commented Jul 2 at 6:07
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.

23
  • 2
    @cppbest "Is it a bug?..." Looks like so and implementation detail. Though the output should match the behavior which here doesn't. Commented Jul 1 at 4:44
  • 3
    fyi: gcc, clang, MSVC all agree on 12 being the correct output - live - godbolt.org/z/5vYPWxKnM Commented Jul 1 at 5:55
  • 7
    I don't see how the paragraph about "evaluated each time" proves anything. auto f() { return []{}; } also evaluates the lambda each time, and it's definitely not a different type each time.
    – Passer By
    Commented Jul 1 at 6:41
  • 3
    It will be evaluated twice, but that doesn't say anything about its type, see the example in previous comment.
    – Passer By
    Commented Jul 1 at 6:53
  • 2
    There is no bug. Lambda is declared once, there is only one lambda type, it's evaluated many times.
    – Gene
    Commented Jul 1 at 8:28
6
+300

This all comes down to the interpretation of [expr.prim.lambda.closure]/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.

What does "unique" mean?

"The type of a lambda-expression... is... unique..."

The first word is "the". The type. Implying that a lambda-expression has one type. But since it's "unique", that means that any two lambda-expressions have different types.

The word "lambda-expression" is italicized, denoting a grammar term. A lambda appearing lexically once, but evaluated more than once, is the same lambda-expression on each evaluation. So it has the same type in each evaluation.

The fact that a default argument is evaluated every time a function is called does not mean that the program behaves as if the default argument were repeated verbatim at each call site. A default argument is a piece of code that runs whenever it's used, just like a function body.

Note, however, that instantiating a template does stamp out a copy of each grammar production that occurs in the template definition (though, for name lookup purposes, it's not the same as "replaying the tokens" at the instantiation point). In other words, if you have a lambda-expression inside a template and you instantiate that template, the resulting specialization has its own lambda-expression that is the result of instantiating the one from the template. Thus, each specialization gets a distinct type for the lambda, even though those lambdas were all defined by the same original piece of source code.

There are also cases where two lambdas appearing in different translation units actually have the same type. This occurs because there is a rule that can force multiple identical pieces of source code from different translation units to behave as if only one copy in the program. [basic.def.odr]/17

2
  • So, there's a case (templates) when the same token-sequence can denote lambdas of different types. Does that contradicts the statement "A lambda appearing lexically once (...) is the same lambda-expression on each evaluation" (in that case, is there any rule that says that the above is possible only in templates?) or is it considered that the instantiations have different tokens?
    – cppbest
    Commented Jul 2 at 4:50
  • 1
    @cppbest I thought I explained this already in my answer. Yes, each time you instantiate a template, you get a copy of each grammar production, i.e., you get a different lambda-expression.
    – Brian Bi
    Commented Jul 2 at 23:29
6

You are right that each lambda expression is associated with a unique closure type, however it is the expression itself, not how many times it is evaluated, which determines the type.

Because we are talking about a lambda expression, we are dealing with a prvalue. Evaluating a prvalue initialises an object, which has exactly one type. [expr.prim.lambda.closure] [dcl.fnc.default] [basic.lval] [intro.object]

A lambda-expression is a prvalue whose result object is called the closure object.

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.

A default argument is evaluated each time the function is called with no argument for the corresponding parameter.

A prvalue is an expression whose evaluation initializes an object or computes the value of an operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.

The properties of an object are determined when the object is created. An object can have a name. An object has a storage duration which influences its lifetime. An object has a type.

An expression remains the same type throughout execution [defns.static.type]

static type

type of an expression resulting from analysis of the program without considering execution semantics

12 is the correct result for your example.

12
  • 2
    The question is language-lawyered. The whole point is to find a standard reference/clause. Many times all three compilers accept ill-formed or wrong program. Commented Jul 1 at 8:25
  • 1
    That's the task. Commented Jul 1 at 8:29
  • 5
    @user12002570 no, you are just wildly misunderstanding definition vs evaluation
    – Caleth
    Commented Jul 1 at 8:47
  • 1
    @Caleth No, I know exactly what they mean. That's why the suggested modification in the cwg issue. The edited answer still doesn't explain anything about the question asked. OP already knows object have a type and a lambda has a closure-type(which they called lambda-type). Commented Jul 1 at 8:47
  • 3
    @Alan there is one expression, which has one type. "The type of a lambda-expression is a unique, unnamed non-union class type"
    – Caleth
    Commented Jul 1 at 9:03
3

I do not have formal prove about this, but I suspect that it will work as you expected in single translation unit, but if you are using multiple translation units then you will have multiple instances. Basically it will work as function defined in header as:

static inline int like_lambda()
{
    static int x = 0; return ++x;
}

here is cpp insight.

Here is prove it do not work as desired when multiple translation units are used:

// side.h
#ifndef SIDE_H
#define SIDE_H

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

#endif
// side.cpp
#include "side.h"
#include <iostream>

int foo(int x) { return x; };

void side_test()
{
    std::cout << foo() << foo() << '\n'; 
}
// main.cpp
#include <iostream>
#include "side.h"

int main() {
    std::cout << foo() << foo() << '\n';
    side_test();
}

IMO best workaround is to fall back to function overload and makes code simpler:

// side.h
#ifndef SIDE_H
#define SIDE_H

int foo(int x);
int foo();
void side_test();

#endif
// side.cpp
#include "side.h"
#include <iostream>

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

int foo(int x) { return x; };

void side_test()
{
    std::cout << foo() << foo() << '\n'; 
}
2
  • 2
    that shows that lambda has internal linkage, I don't think it's possible to create a lambda with an external linkage
    – Gene
    Commented Jul 1 at 17:23
  • here is a way to 'create' an external linkage with a lambda inline auto fun() { return []{ static int x = 0; return ++x; }(); }
    – Gene
    Commented Jul 1 at 17:35
-1

When the foo function is defined, the lambda expression inside the default argument is created and its closure is established. The lambda captures a static int x within its closure. This means the variable x is associated with the lambda itself, not a specific function call. Each call to foo without an argument evaluates the lambda, incrementing the static variable within the closure. This is why you get 12 as the output, as x is incremented from 0 to 1 on the first call and then from 1 to 2 on the second call

2
  • Could you please elaborate on "the lambda expression inside the default argument is created and its closure is established"? I'm interested in refs to the standard
    – cppbest
    Commented Jul 3 at 12:52
  • The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type's function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type's function call operator. timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#6 Commented Jul 3 at 18:21

Not the answer you're looking for? Browse other questions tagged or ask your own question.