为什么在lambda中按引用捕获变量不会改变变量类型?

7

我原本认为通过引用捕获会改变变量的类型。我们来看下面这个例子:

#include <cassert>
#include <type_traits>

int main()
{
    int x = 0;
    int& x_ref = x;
    const int x_const = x;
    const int& x_const_ref = x_const;

    auto lambda = [&]()
    {
        static_assert(std::is_same<decltype(x), int>::value, "!");
        static_assert(std::is_same<decltype(x_ref), int&>::value, "!");
        static_assert(std::is_same<decltype(x_const), const int>::value, "!");
        static_assert(std::is_same<decltype(x_const_ref), const int&>::value, "!");
    };

    lambda();
}

所有检查都没有失败,因此原始变量的类型得以保留。那么,通过引用捕获到底是如何工作的呢?我认为如果用户通过引用捕获,将会在本地作用域中引入具有相同名称的新变量,以引用原始变量的类型。但似乎并非如此。

问题:这样做的动机是什么?还是我误解了什么?


1
decltype 给出了变量的声明类型;在 lambda 函数内部并不影响声明,因此也不会影响 decltype - ildjarn
1
@Incomputable:简单捕获并不是声明,正如我们所知,对于 decltype 来说,重要的是声明。 - ildjarn
1
对于进一步使用被复制捕获的内容进行实验,我建议使用decltype((n))而不是decltype(n),因为后者总是指向原始声明,而前者的行为就好像它指向闭包类型的成员一样(即使名称实际上没有被捕获!)。然后它将给出表达式的类型。据我所知,使用decltype无法获取闭包类型的成员声明的类型。您要么获取原始声明的类型,要么获取闭包成员引用的表达式类型(对于mutable/默认lambda有所区别)。 - Johannes Schaub - litb
1
同样相关的链接:http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1913和http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2011 - Johannes Schaub - litb
1
顺便提一下,您不需要使用 #include <cassert> 就可以使用 static_assert - Oktalist
显示剩余9条评论
2个回答

4

让我们从简单的开始:

int x = 0;
auto lambda = [=]{ return x; };

在这里,return x 实际上是返回 lambda 的成员变量的值。但实际上并非如此。Lambda 表达式没有名为 x 的成员 - lambda 的数据成员本身是无名的。发生的情况是:

lambda 表达式中复制捕获的实体的复合语句内的每个 id-expression 都被转换为对闭包类型的相应无名数据成员的访问。

当你使用 x 时,实际上是在引用相应的成员变量。因此,上述表达式可以解释为:

struct __unnamed {
    int _1;

    __unnamed(int i) : _1(i) { }
    void operator=(__unnamed const&) = delete;
    int operator() const { return _1; }
};

__unnamed lambda(x);

我说这个的意思是...这里只有一个x。即int x变量。所以当你使用decltype(x)时,它会给出x声明类型。当然,这是int。如果你通过引用捕获,这并不会改变,因为lambda的捕获不会改变被捕获变量的类型。
现在,如果你确实想要访问那些未命名的lambda成员,你可以这样做。但是你需要使用decltype((x))而不是decltype(x)

每个decltype((x))的出现,其中x是可能带括号的id-expression,它命名了具有自动存储期的实体,就好像x被转换为对应闭包类型的数据成员的访问一样,如果x是对所述实体的odr-use,则该闭包类型将被声明。

也就是说,在你的例子中,decltype(x)intdecltype(x_const)int const...decltype((x))int&decltype((x_const))int const&。这可能更符合你实际想要的结果。因为现在我们实际上是在引用lambda的成员,或者至少像是在引用。但是请注意,通常的decltype((id))规则仍然适用,所以你将始终得到lvalue的引用类型。

@skypjack,我不理解你的评论。 - Barry
@Barry,我的意思是,你说过 - 因为现在我们实际上是在引用lambda的成员,而lambda的成员必须是引用 - 因为这是你指定的。这是正确的,但在我看来,他询问的是按引用捕获,而在这种情况下,未命名成员并不是必需的,所以这似乎是无意义的。你的推理中我错过了什么? - skypjack
2
关键点是“这是一个odr-use”。decltype(x)不是odr-use,因此x始终指的是周围作用域中的实体,而不是捕获的实体。 - T.C.
1
此外,您说:“lambda的成员必须是引用 - 因为这就是您指定的内容。”这很令人困惑,因为即使他通过复制捕获,他也将获得引用类型const int&const int& 。因为表达式将是lvalues*,而lambda不是mutable - Johannes Schaub - litb
1
还有相关信息:http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1913 和 http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2011 - Johannes Schaub - litb
显示剩余5条评论

3
我认为如果用户通过引用进行捕获,新变量将以相同名称引入本地作用域以具有原始变量的引用类型。

标准规定(重点在于):

如果隐式或显式捕获实体但未按副本方式捕获,则通过引用捕获实体。未指定是否为通过引用捕获的实体在闭包类型中声明了其他未命名的非静态数据成员。如果声明了这样的非静态数据成员,则它们必须是文字类型。

因此,您的期望是错误的。按引用捕获并不意味着在外部作用域中创建一个本地引用对象。

附带说明如下:

int x;
auto l = [&x](){ return [&x](){} }();

如果你的期望是正确的,x 将会是 lambda l 中的一个 引用的引用,但这在 C++ 中是不存在的。

答案已经很好了。如果适用的话,您能否补充一下动机? - Incomputable
@Incomputable 添加了更多细节。如果您需要更多信息,请告诉我。 - skypjack
int i = 0; auto & j = i; auto & k = j; 按照类比,不应该存在引用的引用。 - Tomilov Anatoliy
@Orient 这有点不同。在您的情况下,您正在使用 something 初始化引用。而 OP 所说的是,局部变量是对外部作用域中变量的引用,因此是对引用的引用 - 而不是初始化的引用。我不知道如何更好地解释它,但我希望你明白了。 - skypjack
1
@skypjack,我不确定这是否是OP所说的。 - Oktalist
@Oktalist 他说 - _如果用户通过引用进行捕获,则会引入具有相同名称的新变量到本地作用域中,以具有原始变量的引用类型_。在这种情况下,我理解为例如 int &原始变量的引用类型 将是对引用的引用。无论如何,只有 OP 可以说明他的意思。 - skypjack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接