我在模板中有一些曲折的代码,它使用@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 ++来处理它昨天检索和编译的主干版本。
答案 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)... );
}
我最初认为它与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
但是,编译器能够进行一些优化,即使在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
的不同地址出现在输出中。