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
的相同地址,并且在调用第一个仿函数之后,正在设置 bar
到 nullptr
,然后当第二个函子 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)... );
}
我最初认为它与 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;
}
但是,即使在 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
的不同地址出现在输出中。
我在模板中有一些曲折的代码,它使用 @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
的相同地址,并且在调用第一个仿函数之后,正在设置 bar
到 nullptr
,然后当第二个函子 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)... );
}
我最初认为它与 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;
}
但是,即使在 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
的不同地址出现在输出中。