Lambda C+的未定义行为+;0x闭包:II
我发现C++0x闭包的使用令人费解。我的首字母和首字母所引起的混乱多于解释。下面我将向您展示一些麻烦的例子,我希望找出代码中存在未定义行为的原因。所有代码都通过GCC4.6.0编译器,没有任何警告 程序1:它有效Lambda C+的未定义行为+;0x闭包:II,lambda,c++11,closures,pass-by-reference,Lambda,C++11,Closures,Pass By Reference,我发现C++0x闭包的使用令人费解。我的首字母和首字母所引起的混乱多于解释。下面我将向您展示一些麻烦的例子,我希望找出代码中存在未定义行为的原因。所有代码都通过GCC4.6.0编译器,没有任何警告 程序1:它有效 #包括 int main(){ 自动累加器=[](整数x){ 返回[=](int y)->int{ 返回x+y; }; }; 自动交流=蓄能器(1); std::cout好吧,当引用对象离开时,引用会变得悬而未决。如果对象a引用了对象B的某个部分,则设计非常脆弱,除非对象a以某种方式
#包括
int main(){
自动累加器=[](整数x){
返回[=](int y)->int{
返回x+y;
};
};
自动交流=蓄能器(1);
std::cout好吧,当引用对象离开时,引用会变得悬而未决。如果对象a引用了对象B的某个部分,则设计非常脆弱,除非对象a以某种方式可以保证对象B的生存期(例如,当a持有对B的共享ptr时,或者两者都在同一范围内)
lambda中的引用也不例外。如果您计划返回对x+=y
的引用,最好确保x
的寿命足够长。这里是作为调用累加器(1)
的一部分初始化的参数int x
。函数参数的寿命在函数返回时结束
我希望找出为什么会有
代码中未定义的行为
每次我处理复杂的lambda时,我都觉得首先将其转换为函数对象形式更容易。因为lambda只是函数对象的语法糖,对于每个lambda,都有一个对应函数对象的一对一映射。本文很好地解释了如何进行转换:
例如,你的2号计划:
#include <iostream>
int main(){
auto accumulator = [](int x) {
return [&](int y) -> int {
return x+=y;
};
};
auto ac=accumulator(1);
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
}
在这里,InnerAccumulator的构造函数将引用x,这是一个局部变量,一旦您退出operator()作用域,它就会消失。因此,是的,正如您所怀疑的那样,您只会得到一个非常好的未定义行为。让我们尝试一些看起来完全无辜的方法:
#include <iostream>
int main(){
auto accumulator = [](int x) {
return [&](int y) -> int {
return x+=y;
};
};
auto ac=accumulator(1);
//// Surely this should be a no-op?
accumulator(666);
//// There are no side effects and we throw the result away!
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
}
当然,这也不是保证的行为。事实上,启用优化后,gcc将消除累加器(666)
调用计算它是死代码,我们再次得到原始结果。这样做完全在它的权限范围内;在一致性程序中,删除调用确实不会影响语义。但是在未定义的行为领域,任何事情都可能发生。
编辑
启用优化后
4
4 3 2
7 6 5
10 9 8
再次,C++没有并且不能提供真正的词汇闭包,其中局部变量的生存期将超出其原始范围。这将需要将垃圾收集和堆为基础的本地语言引入到语言中。
不过,这些都是相当学术性的,因为通过拷贝捕获
x
可以使程序定义良好,并按预期工作:
auto accumulator = [](int x) {
return [x](int y) mutable -> int {
return x += y;
};
};
谢谢你的解释,至少我得到了工作的方向。麻烦的是,你提供的演示并没有解释为什么程序2工作得很好。它实现了词法作用域,其中x作为一个状态存在于main的范围内,正如它应该的那样。问题出在程序4中使用std::函数的某个地方,我是霍宾在这种特殊情况下,我想获得一些关于其内部的信息,但这似乎比看起来更难。但我感谢你的回答,我会继续寻找解释。“你提供的演示并不能解释为什么程序2工作良好”.这是您未定义的行为。2.2号程序使用悬挂引用。它完全可以执行所有操作:立即崩溃、稍后崩溃、抛出异常、给出随机结果(如程序4),发射核导弹,将世界变成放射性的荒原,等等。更糟糕的是,它也可以工作!但幸运的是,其他编译器将显示不同的行为。例如,我的代码中的函数对象“work”与GCC4.6(它打印4 3 2 7 6 5 10 9 8)一起工作,但与MSVC10一起崩溃。(您会遇到访问冲突)。我定义“undefined”当结果不可预测时。程序2通过编译器并生成预期结果,因为x位于main范围内。对于习惯块范围的人来说,这看起来很不寻常,但对于词法范围程序员来说,这是一种默认行为。我不会反驳你的评论,即这是一个不安全编程的示例,但这是一个风格问题。悬而未决的引用将“起作用”在程序4中,std::function对堆栈做了一些事情,这些堆栈会弄乱悬挂引用指向的区域,但它与std::function无关,因为Johannes Dahlström的答案表明,很多事情都会导致这种情况最重要的是:仅仅因为它“有效”并且每次都做相同的事情并不意味着它不是未定义的行为。@Rusty:不幸的是,你无法定义什么是“未定义的行为”是指,因为它是语言标准中定义的一个技术术语。该标准说某些语义结构会导致未定义的行为,如果您的程序包含这些结构,则程序有缺陷。这与不同的编程风格毫无关系,它与您的程序是否有缺陷有关!即使使用一个编译器,使用特定的编译器选项,似乎可以“正常工作”,当星号正确时,没有任何东西可以保证它可以“正常工作”明天。我很高兴收到你的帮助,你的评论是有价值的,切中要害的。但是关于你的最后一句话,我必须说:函数参数的生命周期可能不会在函数返回时结束,这是函数的主要悖论。支持这种现象的证据是程序2.Pr.4的行为与你的行为方式相同我喜欢你对脆弱的D的评论,在C++中编写关闭时,似乎需要不断地思考它们的真实的内部属性,这是不可忽视的。
#include <iostream>
int main(){
auto accumulator = [](int x) {
return [&](int y) -> int {
return x+=y;
};
};
auto ac=accumulator(1);
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
}
#include <iostream>
struct InnerAccumulator
{
int& x;
InnerAccumulator(int& x):x(x)
{
}
int operator()(int y) const
{
return x+=y;
}
};
struct Accumulator
{
InnerAccumulator operator()(int x) const
{
return InnerAccumulator(x); // constructor
}
};
int main()
{
Accumulator accumulator;
InnerAccumulator ac = accumulator(1);
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
}
InnerAccumulator operator()(int x) const
{
return InnerAccumulator(x); // constructor
}
#include <iostream>
int main(){
auto accumulator = [](int x) {
return [&](int y) -> int {
return x+=y;
};
};
auto ac=accumulator(1);
//// Surely this should be a no-op?
accumulator(666);
//// There are no side effects and we throw the result away!
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
}
669 668 667
672 671 670
675 674 673
auto ac=accumulator(1);
std::cout << pow(2,2) << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
std::cout << ac(1) << " " << ac(1) << " " << ac(1) << " " << std::endl;
4
1074790403 1074790402 1074790401
1074790406 1074790405 1074790404
1074790409 1074790408 1074790407
4
4 3 2
7 6 5
10 9 8
auto accumulator = [](int x) {
return [x](int y) mutable -> int {
return x += y;
};
};