Wil*_*lly 22 c++ lambda language-lawyer c++11
我发现如果我使用 lambda 来捕获对具有 mutable 关键字的全局变量的引用,然后修改 lambda 函数中的值,则结果在编译器之间是不同的。
#include <stdio.h>
#include <functional>
int n = 100;
std::function<int()> f()
{
int &m = n;
return [m] () mutable -> int {
m += 123;
return m;
};
}
int main()
{
int x = n;
int y = f()();
int z = n;
printf("%d %d %d\n", x, y, z);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
结果来自 VS 2015 和 GCC (g++ (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609):
100 223 100
Run Code Online (Sandbox Code Playgroud)
结果来自 clang++(clang 版本 3.8.0-2ubuntu4 (tags/RELEASE_380/final)):
100 223 223
Run Code Online (Sandbox Code Playgroud)
为什么会发生这种情况?C++ 标准允许这样做吗?
Rem*_*eau 16
lambda 不能按值捕获引用本身(std::reference_wrapper
用于该目的)。
在您的 lambda 中,按值[m]
捕获m
(因为&
捕获中没有),因此m
(作为对 的引用n
)首先被取消引用,并且它所引用的事物的副本( n
) 被捕获。这与这样做没有什么不同:
int &m = n;
int x = m; // <-- copy made!
Run Code Online (Sandbox Code Playgroud)
然后 lambda 修改该副本,而不是原始副本。正如预期的那样,这就是您在 VS 和 GCC 输出中看到的情况。
Clang 输出是错误的,如果还没有,应该报告为错误。
如果您希望 lambda 修改n
,请m
改为通过引用捕获:[&m]
。这与将一个引用分配给另一个引用没有什么不同,例如:
int &m = n;
int &x = m; // <-- no copy made!
Run Code Online (Sandbox Code Playgroud)
或者,您可以m
完全摆脱并n
通过引用捕获:[&n]
.
虽然,由于n
在全局范围内,它确实根本不需要捕获,但 lambda 可以在不捕获它的情况下全局访问它:
return [] () -> int {
n += 123;
return n;
};
Run Code Online (Sandbox Code Playgroud)
我认为 Clang 实际上可能是正确的。
根据[lambda.capture]/11,仅当它构成odr-use 时,lambda 中使用的id 表达式才指代 lambda 的 by-copy-captured 成员。如果不是,则它指的是原始实体。这适用于自 C++11 以来的所有 C++ 版本。
根据 C++17 的[basic.dev.odr]/3,如果对引用变量应用左值到右值转换会产生一个常量表达式,则不会使用 odr。
然而,在 C++20 草案中,左值到右值转换的要求被删除,相关段落多次更改以包含或不包含转换。请参阅CWG 问题 1472和CWG 问题 1741,以及开放的CWG 问题 2083。
由于m
使用常量表达式(指静态存储持续时间对象)初始化,因此使用它会在[expr.const]/2.11.1 中为每个异常生成一个常量表达式。
但是,如果应用了左值到右值的转换,则情况并非如此,因为 的值n
在常量表达式中不可用。
因此,根据是否应该在确定 odr 使用时应用左值到右值转换,当您m
在 lambda 中使用时,它可能会或可能不会指代 lambda 的成员。
如果应该应用转换,则 GCC 和 MSVC 是正确的,否则 Clang 是正确的。
如果您将 的初始化更改m
为不再是常量表达式,您可以看到 Clang 改变了它的行为:
#include <stdio.h>
#include <functional>
int n = 100;
void g() {}
std::function<int()> f()
{
int &m = (g(), n);
return [m] () mutable -> int {
m += 123;
return m;
};
}
int main()
{
int x = n;
int y = f()();
int z = n;
printf("%d %d %d\n", x, y, z);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,所有编译器都同意输出是
100 223 100
Run Code Online (Sandbox Code Playgroud)
因为m
在lambda将指的是封闭的部件,其类型int
与参考可变副本初始化m
中f
。