Lambda C+的未定义行为+;0x闭包:II

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以某种方式

我发现C++0x闭包的使用令人费解。我的首字母和首字母所引起的混乱多于解释。下面我将向您展示一些麻烦的例子,我希望找出代码中存在未定义行为的原因。所有代码都通过GCC4.6.0编译器,没有任何警告

程序1:它有效
#包括
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;
    }; 
};