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


22

我发现如果我使用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;
}

VS 2015和GCC的结果(g ++(Ubuntu 5.4.0-6ubuntu1〜16.04.12)5.4.0 20160609):

100 223 100

clang ++(clang版本3.8.0-2ubuntu4(标签/ RELEASE_380 / final))的结果:

100 223 223

为什么会这样?C ++标准允许吗?


Clang的行为仍然存在于中继线上。
胡桃木

这些都是相当旧的编译器版本
MM

它仍然在最近的Clang版本中显示:godbolt.org/z/P9na9c
Willy

1
如果您完全删除捕获,那么GCC仍然会接受此代码并执行clang的操作。这有力地暗示了存在GCC错误-简单捕获不应更改lambda主体的含义。
TC

Answers:


16

Lambda无法通过值本身捕获引用(std::reference_wrapper用于此目的)。

在你的拉姆达,[m]捕获m的值(因为没有&在捕捉),所以m(是一个参考n)首先取消引用和副本的它引用的东西(n)被捕获。这与这样做没有什么不同:

int &m = n;
int x = m; // <-- copy made!

然后,lambda修改该副本,而不是原始副本。正如您所期望的,这就是您在VS和GCC输出中看到的。

Clang输出错误,如果尚未输出,则应将其报告为错误。

如果您希望自己的lambda进行修改n,请m改为通过引用捕获:[&m]。这与将一个引用分配给另一个引用没有什么不同,例如:

int &m = n;
int &x = m; // <-- no copy made!

或者,您可以m完全摆脱并n通过引用进行捕获:[&n]

尽管由于n处于全局范围内,所以实际上根本不需要捕获它,但是lambda可以在不捕获的情况下全局访问它:

return [] () -> int {
    n += 123;
    return n;
};

5

我认为Clang实际上可能是正确的。

根据[lambda.capture] / 11,仅当lambda构成odr-use时,该lambda中使用的id表达式才引用该lambda的被副本捕获的成员。如果不是,则引用原始实体。这适用于C ++ 11以后的所有C ++版本。

根据C ++ 17的[basic.dev.odr] / 3,如果对引用变量进行从左值到右值的转换会产生一个常量表达式,则不会使用该引用变量。

但是,在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;
}

在这种情况下,所有编译器都同意输出为

100 223 100

因为m在lambda中将引用闭包的成员,该成员的类型是int根据中的引用变量m进行了复制初始化的f


VS / GCC和Clang的结果都正确吗?还是只有其中之一?
威利

[basic.dev.odr] / 3表示变量m是由命名该表达式的表达式使用的,除非对其应用从左值到右值的转换将是一个常量表达式。通过[expr.const] /(2.7),该转换将不是核心常量表达式。
aschepler

如果Clang的结果正确,我认为这有点违反直觉。因为从程序员的角度来看,他需要确保他写在捕获列表中的变量实际上是出于可变情况而被复制的,并且由于某些原因,稍后可能会更改m的初始化。
威利

1
m += 123;m是使用过的。
奥利夫

1
我认为Clang用当前的措词是正确的,尽管我没有对此进行深入研究,但这里的相关更改几乎可以肯定是所有DR。
TC

4

C ++ 17标准不允许这样做,但是其他一些标准草案可能允许这样做。这很复杂,原因未在此答案中说明。

[expr.prim.lambda.capture] / 10

对于通过副本捕获的每个实体,在闭包类型中声明一个未命名的非静态数据成员。这些成员的声明顺序未指定。如果实体是对对象的引用,则此数据成员的类型为引用的类型;如果实体是对函数的引用,则为对引用的函数类型的左值引用;否则为相应捕获的实体的类型。

[m]装置,该变量mf被拷贝捕获。实体m是对对象的引用,因此闭包类型具有一个成员,其类型为引用的类型。也就是说,成员的类型为int,而不是int&

由于该名称m在lambda体名内关闭对象的成员,而不是在变量f(这是有问题的部分),语句m += 123;修改该成员,这是一个不同int的对象::n

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.