C++ lambda 在模板的第二次扩展中没有捕获变量?

C++ lambda not capturing variable on 2nd expansion in template?

我在模板中有一些曲折的代码,它使用 @R. Martinho Fernandes 的技巧循环展开可变参数模板中的一些打包参数,并对参数列表中的每个参数调用相同的代码。

但是,似乎好像 lambda 没有被正确初始化,而是在 functor(?) 实例之间共享变量,这似乎是错误的。

鉴于此代码:

#include <iostream>
#include <functional>

template<typename... Args>
void foo(Args ... args) {
  int * bar = new int();
  *bar = 42;

  using expand_type = int[];
  expand_type{(
    args([bar]() {
        std::cerr<<std::hex;
        std::cerr<<"&bar="<<(void*)&bar<<std::endl;
        std::cerr<<"  bar="<<(void*)bar<<std::endl;
        std::cerr<<"  bar="<<*bar<<std::endl<<std::endl;
    }),
    0) ... 
  };
};

int main() {
  std::function<void(std::function<void()>)> clbk_func_invoker = [](std::function<void()> f) { f(); };
  foo(clbk_func_invoker, clbk_func_invoker);

  return 0;
}

我得到以下输出:

&bar=0x7ffd22a2b5b0
  bar=0x971c20
  bar=2a

&bar=0x7ffd22a2b5b0
  bar=0
Segmentation fault (core dumped)

所以,我相信我看到的是两个仿函数实例共享捕获变量 bar 的相同地址,并且在调用第一个仿函数之后,正在设置 barnullptr,然后当第二个函子 seg'-faults 试图取消引用 same bar 变量(在完全相同的地址)时出错。

仅供参考,我意识到我可以通过将 [bar](){... 函子移动到变量 std::function 变量中然后捕获该变量来解决这个问题。但是,我想了解 为什么 第二个仿函数实例使用完全相同的 bar 地址以及为什么它获得 nullptr 值。

我运行这是用 GNU 的 g++ 针对他们昨天检索和编译的主干版本。

首先我没有解决方案,我想添加这个额外的信息作为评论,但不幸的是我还不能发表评论。

我用 Intel 17 c++ 编译器试过你之前的代码并且工作正常:

&bar=0x7fff29e40c50
  bar=0x616c20
  bar=2a

&bar=0x7fff29e40c50
  bar=0x616c20
  bar=2a

在某些情况下,&bar(用于存储捕获值的新变量的地址)在第一次调用和第二次调用之间不同,但它也有效。

我还用 GNU 的 g++ 尝试了您的代码,将 bar 的类型从 int* 更改为 int。即使在这种情况下,在第二次和后续调用中捕获的值也是错误的:

&bar=0x7fffeae12480
  bar=2a
&bar=0x7fffeae12480
  bar=0
&bar=0x7fffeae12480
  bar=0

最后我尝试稍微修改一下代码并按值和对象传递,因此必须调用复制构造函数:

#include <iostream>
#include <functional>

struct  A {
    A(int x) : _x(x) { 
      std::cerr << "Constructor!" << n++ << std::endl;
    }
    A(const A& a) : _x(a._x) {
      std::cerr << "Copy Constructor!"  << n++ << std::endl;
    }
    static int n;
    int _x;  
};

int A::n = 0;

template<typename... Args>
void foo(Args ... args) {
  A a(42);

  std::cerr << "-------------------------------------------------" << std::endl;
  using expand_type = int[];
  expand_type  {
   (args( [a]() {
          std::cerr << "&a, "<< &a << ", a._x," << a._x << std::endl;
         }
       ),
    0) ... 
  };
std::cerr << "-------------------------------------------------" << std::endl;
}

int main() {
  std::function<void(std::function<void()>)> clbk_func_invoker = [](std::function<void()> f) { f(); };
  foo(clbk_func_invoker, clbk_func_invoker, clbk_func_invoker);
  return 0;
}

我当前的 g++ (g++ (GCC) 6.1.0) 版本无法编译此代码。我也尝试了 Intel 并且它有效,虽然我不完全理解为什么复制构造函数被调用了这么多次:

Constructor!0
-------------------------------------------------
Copy Constructor!1
Copy Constructor!2
Copy Constructor!3
&a, 0x617c20, a._x,42
Copy Constructor!4
Copy Constructor!5
Copy Constructor!6
&a, 0x617c20, a._x,42
Copy Constructor!7
Copy Constructor!8
Copy Constructor!9
&a, 0x617c20, a._x,42
-------------------------------------------------

到目前为止我测试的就是这些。

参数包其中包含 lambda 表达式 往往会让编译器适应。避免这种情况的一种方法是将扩展部分和 lambda 部分分开。

template<class F, class...Args>
auto for_each_arg( F&& f ) {
  return [f=std::forward<F>(f)](auto&&...args){
    using expand_type = int[];
    (void)expand_type{0,(void(
      f(decltype(args)(args))
    ),0)...};
  };
}

这需要一个 lambda f 和 returns 一个将在其每个参数上调用 f 的对象。

然后我们可以重写 foo 来使用它:

template<typename... Args>
void foo(Args ... args) {
  int * bar = new int();
  *bar = 42;

  for_each_arg( [bar](auto&& f){
    f( [bar]() {
      std::cerr<<std::hex;
      std::cerr<<"&bar="<<(void*)&bar<<std::endl;
      std::cerr<<"  bar="<<(void*)bar<<std::endl;
      std::cerr<<"  bar="<<*bar<<std::endl<<std::endl;
    } );
  } )
  ( std::forward<Args>(args)... );
}

live example.

我最初认为它与 std::function 构造函数有关。它不是。 A simpler example 没有 std::function 以同样的方式崩溃:

template<std::size_t...Is>
void foo(std::index_sequence<Is...>) {
  int * bar = new int();
  *bar = 42;

  using expand_type = int[];
  expand_type{(
    ([bar]() {
      std::cerr<<"bar="<<*bar<<'\n';
    })(),
    (int)Is) ... 
  };
}

int main() {
  foo(std::make_index_sequence<2>{});

  return 0;
}

we can invoke the segfault without the cerr,给我们更容易阅读的反汇编:

void foo<3, 0ul, 1ul>(std::integer_sequence<unsigned long, 0ul, 1ul>)::{lambda()#1}::operator()() const:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    (%rax), %rax
    movl    , (%rax)
    nop
    popq    %rbp
    ret
void foo<3, 0ul, 1ul>(std::integer_sequence<unsigned long, 0ul, 1ul>):
    pushq   %rbp
    movq    %rsp, %rbp
    pushq   %rbx
    subq    , %rsp
    movl    , %edi
    call    operator new(unsigned long)
    movl    [=13=], (%rax)
    movq    %rax, -24(%rbp)
    movq    -24(%rbp), %rax
    movl    , (%rax)
    movq    -24(%rbp), %rax
    movq    %rax, -48(%rbp)
    leaq    -48(%rbp), %rax
    movq    %rax, %rdi
    call    void foo<3, 0ul, 1ul>(std::integer_sequence<unsigned long, 0ul, 1ul>)::{lambda()#1}::operator()() const
    movabsq $-4294967296, %rax
    andq    %rbx, %rax
    movq    %rax, %rbx
    movq    [=13=], -32(%rbp)
    leaq    -32(%rbp), %rax
    movq    %rax, %rdi
    call    void foo<3, 0ul, 1ul>(std::integer_sequence<unsigned long, 0ul, 1ul>)::{lambda()#1}::operator()() const
    movl    %ebx, %edx
    movabsq 94967296, %rax
    orq     %rdx, %rax
    movq    %rax, %rbx
    nop
    addq    , %rsp
    popq    %rbx
    popq    %rbp
    ret

我还没有解析反汇编,但在玩第一个时,它显然破坏了第二个 lambda 的状态。

经过几次测试,我发现都是关于 lambda 的评估而不是包扩展。

你所拥有的是一组 lambda,它们在包扩展完成之前不会执行,因此在执行时它们都观察到相同的变量实例,如果每个 lambda 的执行都会不同与扩展的顺序相对应,然后每个扩展都会得到它自己的变量副本,并且 lambda 将被视为物化 prvalue 其生命周期已结束:

template<typename... Args>
void foo(Args ... args) {
    int * bar = new int();
    *bar = 42;

    using expand_type = int[];
    expand_type{( args([bar]{
       std::cerr<<std::hex;
       std::cerr<<"&bar="<<(void*)&bar<<std::endl;
       std::cerr<<"  bar="<<(void*)bar<<std::endl;
       std::cerr<<"  bar="<<*bar<<std::endl<<std::endl;
       return 0;
    }()),0) ...
  };
};

int main() {
    std::function<void(int)> clbk_func_invoker = [](int) {  };    
    foo(clbk_func_invoker, clbk_func_invoker);    
    return 0;
}

live example.

但是,即使在 capture by copy 下扩展 trivial classes 时扩展评估的 lambda 而未执行,编译器也能够进行一些优化。

举个更简单的例子:

struct A{ };

template<class... T>
auto foo(T... args){
  A a;
  std::cout<< &a << std::endl;
  using expand = int[];

 expand{ 0,(args([a] { 
      std::cout << &a << " " << std::endl; return 0; }),void(),0)... 
 };
}

foo([](auto i){ i(); }, [](auto i){  i(); }); 

将为每个扩展的 lambda 输出相同的地址 a,即使期望 a 的单独副本也是如此。因为 capture by copy 产生复制变量的常量版本并且没有这些副本不能进行突变,因为 trivial classes 是一种通过所有扩展的 lambda 共享同一个实例的性能(因为不能保证改变).

但如果现在的类型不是普通类型,则优化已被破坏并且每个扩展的 lambda 都需要不同的副本:

 struct A{ 
    A() = default;
    A(const A&){}
 };

A 上的此更改导致 a 的不同地址出现在输出中。