lambda 函数从全局变量的引用中可变捕获的行为差异

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)


wal*_*nut 5

我认为 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 问题 1472CWG 问题 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与参考可变副本初始化mf