在类分段错误中存储Lambda

Storing Lambda in class segfaults

本文关键字:存储 Lambda 错误 分段      更新时间:2023-10-16

这是我的程序的简化版本,以说明这个问题:

#include <functional>
#include <iostream>
template<class T>
class Piped {
public:
    typedef T value_type;
};
template<class P1, class P2, class T>
class PipeConnector : public Piped<T> {
public:
    PipeConnector(const P1 &p1, const P2 &p2)
     : m_1(p1), m_2(p2) { }
    bool run(const T &element) const {
        return m_1.run(element) || m_2.run(element);
    }
private:
    const P1 &m_1;
    const P2 &m_2;
};
template<class T>
class NullOp : public Piped<T> {
public:
    bool run(const T&) const {
        return false;
    }
};
template<class T, class Functor>
class FunctionOp : public Piped<T> {
public:
    FunctionOp(Functor f1)
     : m_1(f1) { }
    bool run(const T &element) const {
        return m_1(element);
    }
private:
    std::function<bool(T)> m_1;
};
template<class P1, class Functor>
auto operator|(const P1 &p1, const Functor &f2) {
    return PipeConnector<P1,
            FunctionOp<typename P1::value_type, std::function<bool(typename P1::value_type)>>, typename P1::value_type>(
                    p1, FunctionOp<typename P1::value_type, std::function<bool(typename P1::value_type)>>(f2));
}
int main() {
    auto p = NullOp<int>() | [](int x) -> bool { if (x < 10) { return true;} return false; };
    std::cout << p.run(20) << std::endl;
    return 0;
}

使用g++/clang++ -std=c++14编译这个程序会导致段错误。向它添加-O3,使它运行时没有段错误。

当将PipeConnector更改为不存储const引用而存储副本时,此操作有效。我认为问题是一些lambda作用域问题,但我不明白是什么出了问题。o3似乎忽略了这个问题?你能给我解释一下这个问题吗?

问题是NullOp<int>()是在PipedConnector中存储const-reference的临时对象。这个临时变量具有完整的表达式生存期,因此在p初始化之后它不存在。当您调用p.run(20)时,您将再次引用那个已被删除的临时对象。由此产生的UB可能导致崩溃。

您的operator|()创建了一个PipeConnector对象,其两个参数都是临时的。然后,PipeConnector构造函数返回一个对象,其中包含对这些临时对象的引用。当调用该对象的run方法时,临时对象的生存期已经过期。

NullOp对象没有状态,它的引用是悬空的(在实践中)可能并不重要,但是由lambda构造的std::function对象就不是这样了。这和本身不一样;lambda本身是无状态的,但是std::function操作符包含一个函数指针。

我想使用-O3编译器可以找出在调用std::function成员变量时应该调用什么函数,因此它内联调用。但它仍然是UB,即使它看起来有效。

Edit: OP建议延长临时对象的生存期,因为它绑定到一个引用。但是,在这种情况下,这并不适用。参见const引用是否会延长临时对象的寿命?

在调用operator|()时创建的PipeConnector的构造函数中有三个用临时对象初始化的引用。其中第一个是NullOp<int>(),是operator|()函数的参数,它的生命周期不会被§12.2 [class]的措辞显式地延长。[临时]第5.1段:

在函数调用(5.2.2)中绑定到引用形参的临时对象一直存在,直到包含该调用的完整表达式完成为止。

另外两个临时对象是std::function<bool(int)>的实例(lambdaFunctionOp<int, std::function<bool(int)>>的初始化器中使用)和FunctionOp本身的转换结果。在2013年和2014年上半年的标准草案版本中,这种情况(与函数调用情况非常相似)有类似的措辞:

构造函数的参数初始化器(12.6.2 [class.base.init])中的引用成员的临时绑定将一直存在,直到构造函数退出。

然而,这句话在DR 1696的拟议决议中被删除了(2014年9月29日提交给标准草案)。我相信删去这句话是为了清楚;据我所知,DR1696决议中的其他修改,特别是§12.6.2:

中的新第8段,都将上述情况标记为无效。

在mem初始化式中绑定到引用成员的临时表达式是病态的。