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

时间:2016-08-24 16:19:33

标签: c++ c++11 gcc lambda variadic-templates

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

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

鉴于此代码:

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

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

我得到以下输出:

bar

所以,我相信我看到的是两个仿函数实例共享捕获变量bar的相同地址,并且在调用第一个仿函数后,nullptr正在被设置到bar,然后是第二个仿函数,当它试图取消引用相同的 [bar](){...变量时(在完全相同的地址中)。

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

我使用GNU的g ++来处理它昨天检索和编译的主干版本。

3 个答案:

答案 0 :(得分:3)

带有lambdas的参数包 倾向于让编译器适合。避免这种情况的一种方法是将扩展部分和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并返回一个对象,该对象将在每个参数上调用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构造函数有关。它不是。没有std::function的{​​{3}}以同样的方式崩溃:

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;
}

A simpler example,为我们提供了更易于阅读的反汇编:

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    $3, (%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    $40, %rsp
    movl    $4, %edi
    call    operator new(unsigned long)
    movl    $0, (%rax)
    movq    %rax, -24(%rbp)
    movq    -24(%rbp), %rax
    movl    $42, (%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    $0, -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 $4294967296, %rax
    orq     %rdx, %rax
    movq    %rax, %rbx
    nop
    addq    $40, %rsp
    popq    %rbx
    popq    %rbp
    ret

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

答案 1 :(得分:2)

首先,我没有解决方案,我想将此额外信息添加为评论,但遗憾的是我还无法发表评论。

我使用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)无法编译此代码。我也试过英特尔并且它有用,虽然我不完全理解为什么复制构造函数被多次调用:

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
-------------------------------------------------

到目前为止我所测试的全部内容。

答案 2 :(得分:1)

经过几次测试后,我发现所有这些都是关于lambdas的评估而不是包扩展。

你所拥有的是一组lambdas,它们在包扩展完成之前不执行,因此在执行时它们都会观察到相同的变量实例,如果每个lambda的执行对应于扩展的顺序,然后每个扩展将获得它自己的变量副本,lambda将被视为生命周期已经结束的物化private List<string> NetworkHosts { get { var result = new List<string>(); var root = new DirectoryEntry("WinNT:"); foreach (DirectoryEntry computers in root.Children) { result.AddRange(from DirectoryEntry computer in computers.Children where computer.Name != "Schema" select computer.Name); } return result; } }

prvalue

live example.

但是,编译器能够进行一些优化,即使在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; } 下扩展trivial classes时展开评估的lambda并且不执行。

让我们举一个更简单的例子:

capture by copy

为每个扩展的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(); }); 地址,即使预期会有a的个人副本。由于a生成复制变量的常量版本,并且不能进行任何突变,因此capture by copy是一种通过所有扩展lambda共享同一实例的性能(因为没有保证更改) )。

但是如果现在的类型不是一个简单的类型,那么优化已被打破,每个扩展的lambda都需要不同的副本:

trivial classes

struct A{ A() = default; A(const A&){} }; 上的此更改导致A的不同地址出现在输出中。