#include <iostream>

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

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

我知道每次调用函数时都会评估默认参数。这是否意味着 lambda 类型在每次调用时都不同?如果不是,请指出标准引用来解释为什么会这样。

19

  • 并且


    – 


  • 3
    每个 lambda 表达式的类型都是不同的。多次求值的相同表达式将产生相同的类型。考虑在 for 循环中定义的 lambda。每次迭代都会求值表达式,但 lambda 类型保持不变。


    – 

  • 3
    @user12002570 我不明白这些答案如何解释我的问题中的行为


    – 

  • 2
    真正的问题不是为什么这只创建一种 lambda 类型,而是为什么每次将 lambda 作为默认模板参数都会创建一个新的 lambda。


    – 

  • 2
    中解释的那样,这是


    – 


6 个回答
6

的这个例子非常清楚地表明,其意图是定义默认参数的地方也定义了语义:

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))
  }
}

特别是,默认参数不仅仅是在函数调用时插入然后进行分析的标记序列。

按照这个意图,lambda 表达式是在默认参数定义时进行分析的,而不是在函数调用时。因此,lambda 类型只有一个,而不是多个,正确的结果是12

但是,关于使用 lambda 表达式作为默认参数,标准并没有足够清楚地表达这一点。

3

  • 那么,您指的是这句话“在默认参数出现的位置查找默认参数中的名称,并检查语义约束 (…)”?如果是这样,那么是指名称查找部分(意味着operator()只查找一次)还是语义约束部分(这是什么意思)?这个例子很好,但通常它们是非规范的。


    – 

  • @cppbest 我没有引用任何规范性文本,因为我没有找到任何专门讨论 lambda 表达式的内容。这个例子只是抓住了精神(“意图”):我们在默认参数定义的位置有什么很重要。然后调用站点只使用已定义的内容。对于 lambda 表达式,我们只有一个定义,并且该定义在调用站点使用。


    – 

  • 除非这是在由多个翻译单元使用的头文件中,在这种情况下每个翻译单元都可能产生不同的类型,而链接器可能会或可能不会合并这些类型。


    – 

这是来解释程序的行为,这意味着输出12是正确的。

来自

如果 D 是模板,且在多个翻译单元中定义,则上述要求应适用于模板定义中使用的模板封闭范围的名称,也适用于实例化点 ([temp.dep]) 的依赖名称。这些要求也适用于 D 的每个定义中定义的相应实体(包括 lambda 表达式的闭包类型,但不包括 D 或未在 D 中定义的实体的默认参数或默认模板参数中定义的实体)。对于每个这样的实体和 D 本身,行为就像存在一个具有单个定义的单个实体,包括将这些要求应用于其他实体。

[注 4:实体仍在多个翻译单元中声明,并且 [basic.link] 仍适用于这些声明。特别是,出现在 D 类型中的 lambda 表达式 ([expr.prim.lambda]) 可能导致不同的声明具有不同的类型,并且出现在 D 的默认参数中的 lambda 表达式可能仍表示不同翻译单元中的不同类型。
— 结束注释]

[实施例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);
 }
};

如果 的定义g出现在多个翻译单元中,则程序格式不正确(无需诊断),因为每个这样的定义都使用引用不同 lambda 表达式闭包类型的默认参数。
X 的定义可以出现在有效程序的多个翻译单元中; X 定义中 X​::​h 的默认参数内定义的 lambda 表达式在每个翻译单元中表示相同的闭包类型。 — 示例结束]

请注意,在您的示例中,函数定义foo仅出现在一个翻译单元中。给定的程序格式正确,并且您给出的示例中只有一个闭包类型(在单个 TU 中),因此12根据当前措辞,输出是正确的。

二十七

  • 2
    @cppbest “这是个 bug 吗?…”看起来是这样的,而且有实现细节。虽然输出应该与行为相匹配,但这里却不匹配。


    – 


  • 3
    仅供参考:gcc、clang、MSVC 都同意 12 是正确的输出 – live –


    – 


  • 7
    我不明白关于“每次评估”的段落如何证明任何事情。auto f() { return []{}; }每次也会评估 lambda,而且每次肯定不是不同的类型。


    – 

  • 3
    它将被评估两次,但这并没有说明它的类型,请参见前面评论中的示例。


    – 

  • 2
    没有错误。Lambda 声明一次,只有一种 lambda 类型,它被评估多次。


    – 

您说得对,每个 lambda 表达式都与一个唯一的闭包类型相关联,但是,决定类型的是表达式本身,而不是求值的次数。

因为我们正在讨论 lambda 表达式,所以我们处理的是 prvalue。对 prvalue 求值会初始化一个对象,该对象只有一个类型。

Lambda 表达式是一个 prvalue,其结果对象称为闭包对象。

Lambda 表达式的类型(也是闭包对象的类型)是一种唯一的、未命名的非联合类类型,称为闭包类型,其属性如下所述。

每次调用函数时,都会计算默认参数,而相应形参没有参数。

prvalue 是一个表达式,其求值初始化一个对象或计算一个运算符的操作数的值,如其出现的上下文所指定,或是一个具有 cv void 类型的表达式。

对象的属性在创建时就已确定。对象可以有一个名称。对象具有存储期限,这会影响其生命周期。对象具有类型。

表达式在整个执行过程中保持相同的类型

静态类型

不考虑执行语义而对程序进行分析得出的表达式类型

对于您的例子来说,12 是正确的结果。

十三

  • 2
    这个问题是语言律师的问题。关键是要找到一个标准的参考/条款。很多时候,这三个编译器


    – 


  • 1
    这就是任务。


    – 

  • 4
    @user12002570 不,你只是严重误解了定义与评估


    – 

  • 1
    @Caleth 不,我完全明白他们的意思。这就是 cwg 问题中建议修改的原因。编辑后的答案仍然没有解释所提问题的任何内容。OP 已经知道对象有一个类型,而 lambda 有一个闭包类型(他们称之为 lambda 类型)。


    – 


  • 2
    @Alan 有一个表达式,它有一个类型。“lambda 表达式的类型是唯一的、未命名的非联合类类型”


    – 

这一切都归结为的解释:

Lambda 表达式的类型(也是闭包对象的类型)是一种唯一的、未命名的非联合类类型,称为闭包类型,其属性如下所述。

“独特”是什么意思?

“ Lambda 表达式的类型…是…独一无二的…”

第一个词是“the”。类型。暗示lambda 表达式只有一种类型。但由于它是“唯一的”,这意味着任何两个lambda 表达式都有不同的类型。

“ lambda 表达式”一词以斜体显示,表示语法术语。如果 lambda 在词汇中出现一次,但被求值多次,则每次求值时都是同一个lambda 表达式。因此,每次求值时,其类型都相同。

每次调用函数时都会评估默认参数,但这并不意味着程序的行为就像默认参数在每个调用点都一字不差地重复一样。默认参数是一段每次使用时都会运行的代码,就像函数体一样。

但请注意,实例化模板复制模板定义中出现的每个语法生成(尽管出于名称查找目的,这与在实例化点“重放标记”不同)。换句话说,如果您在模板中有一个lambda 表达式,并且您实例化了该模板,则生成的特化将具有自己的lambda 表达式,这是实例化模板中 lambda 表达式的结果。因此,每个特化都会获得 lambda 的不同类型,即使这些 lambda 都是由同一原始源代码定义的。

还存在两个出现在不同翻译单元中的 lambda 实际上具有相同类型的情况。发生这种情况的原因是存在一条规则,该规则可以强制来自不同翻译单元的多个相同源代码片段表现得像程序中只有一个副本一样。

2

  • 因此,存在一种情况(模板),即相同的标记序列可以表示不同类型的 lambda。这是否与“在词汇上出现一次的 lambda (…) 在每次评估中都是相同的 lambda 表达式”这一说法相矛盾(在这种情况下,是否有任何规则表明上述情况仅在模板中才有可能?)还是认为实例化具有不同的标记?


    – 


  • 1
    @cppbest 我以为我已经在回答中解释过了。是的,每次实例化模板时,都会获得每个语法生成的副本,即获得不同的lambda 表达式


    – 

我没有对此进行正式证明,但我怀疑它会在单个翻译单元中按预期工作,但如果您使用多个翻译单元,那么您将有多个实例。基本上它将作为标头中定义的函数工作:

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

这是

这证明当使用多个翻译单元时

// 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();
}

我认为最好的解决方法是回到函数重载并使代码更简单:

// 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
    这表明 lambda 具有内部链接,我认为不可能创建具有外部链接的 lambda


    – 

  • 这是一种使用 lambda 创建外部链接的方法inline auto fun() { return []{ static int x = 0; return ++x; }(); }


    – 

foo定义函数时,会创建默认参数内的 lambda 表达式并建立其闭包。lambdastatic int x在其闭包内捕获。这意味着变量x与 lambda 本身相关联,而不是特定的函数调用。每次foo不带参数的调用都会评估 lambda,从而增加闭包内的静态变量。这就是为什么您会得到12输出,x在第一次调用时从 0 增加到 1,然后在第二次调用时从 1 增加到 2

2

  • 您能否详细说明“默认参数中的 lambda 表达式已创建并且其闭包已建立”?我对标准的引用很感兴趣


    – 

  • 不带 lambda 捕获的 lambda 表达式的闭包类型具有公共非虚拟非显式 const 转换函数,该转换函数指向具有与闭包类型的函数调用运算符相同的参数和返回类型的函数指针。此转换函数返回的值应为函数的地址,调用该函数时,其效果与调用闭包类型的函数调用运算符相同。timsong


    –