1

考虑一个X带有重载右关联 C++ 运算符的二元运算: a+=b+=c-->Y{a, X{b,c}}

可以从某种语法树(X 和 Y 对象的组合)中的表达式“冻结”有关操作数的所有信息,并在以后访问它。(这不是问题

struct X{Operand& l; Operand& r; /*...*/};
struct Y{Operand& l; X r; /*...*/};
Operand a, b, c;
auto x = Y{a, X{b,c}};
//access members of x...

如果我存储Y::r为值(如上),则将涉及复制或至少移动。如果我存储Y::r为右值引用(例如X&& r;),那么它将引用一个临时的,当表达式结束时将被销毁,给我留下一个悬空的引用。

为了在多个地方多次使用已经构建的表达式,捕获它或防止这种自动破坏的最佳方法是什么?

  • 通过捕捉我的意思是以某种方式延长它的寿命,而不是手动将它分配给局部变量(有效但不能很好地扩展!想想这个案例a+=b+=…+=z:)
  • 我知道移动比复制便宜......但什么都不做更好(对象在那里,已经构建)
  • 您可以将表达式作为右值引用参数传递给函数或 lambda,并在该函数/lambda 内部访问其成员......但您不能重用它(外部)!您必须每次都重新创建它(有人称这种方法为“厄运 Lambda”,也许还有其他缺点)

这是一个测试程序(位于https://godbolt.org/z/7f78T4zn9):

#include <assert.h>
#include <cstdio>
#include <utility>

#ifndef __FUNCSIG__
#   define __FUNCSIG__ __PRETTY_FUNCTION__
#endif

template<typename L,typename R> struct X{
    L& l; R& r;
    X(L& l, R& r): l{l}, r{r} {printf("X{this=%p &l=%p &r=%p} %s\n", this, &this->l, &this->r, __FUNCSIG__);};
    ~X(){printf("X{this=%p} %s\n", this, __FUNCSIG__);};

    X(const X& other) noexcept      = delete;
    X(X&& other) noexcept           = delete;
    X& operator=(const X&) noexcept = delete;
    X& operator=(X&&) noexcept      = delete;
};
template<typename L,typename R> struct Y{
    L& l; R&& r;
    Y(L& l, R&& r): l{l}, r{std::forward<R>(r)} {
        printf("Y{this=%p &l=%p r=%p} %s\n", this, &this->l, &this->r, __FUNCSIG__);
        assert(&this->r == &r);
    };
    ~Y(){printf("Y{this=%p} %s\n", this, __FUNCSIG__);};
    void func(){printf("Y{this=%p} &r=%p ... ALREADY DELETED! %s\n", this, &r, __FUNCSIG__);};
};

struct Operand{
    Operand(){printf("Operand{this=%p} %s\n", this, __FUNCSIG__);}
    ~Operand(){printf("Operand{this=%p} %s\n", this, __FUNCSIG__);}
};

//================================================================
int main(){
    Operand a, b, c;
    printf("---- 1 expression with temporaries\n");
    auto y = Y{a, X{b,c}};//this will come from an overloaded right-associative C++ operator, like: a+=b+=c
    printf("---- 2 immediately after expression... but already too late!\n");//at this point the temporary X obj is already deleted
    y.func();//access members...
    printf("---- 3\n");
    return 0;
}

这是一个输出示例,您可以在其中看到 X 临时对象的地址进入 Y::r ... 并在有机会捕获它之前立即销毁:

---- 1 expression with temporaries
X{this=0x7ffea39e5860 &l=0x7ffea39e584e &r=0x7ffea39e584f} X::X(Operand&, Operand&)
Y{this=0x7ffea39e5850 &l=0x7ffea39e584d r=0x7ffea39e5860} Y::Y(Operand&, X&&)
X{this=0x7ffea39e5860} X::~X()
---- 2 immediately after expression... but already too late!
4

1 回答 1

1

没有办法以您希望的方式延长临时工的寿命。

有几种方法可以延长临时生命。他们中的大多数都没有帮助。例如,在构造函数期间用于成员初始化的临时值将持续到构造函数结束。这在这种表达式树的一个“层”中可能很有用,但对两个没有帮助。

可以延长临时工寿命的一种有趣方式是成为参考对象。

{
    const std::string& x = std::string("Hello") + " World";
    foo();
    std::cout << x << std::endl; // Yep!  Still "Hello World!"
}

这将持续到x超出范围。但它不会做任何事情来延长其他临时人员的生命。 "Hello"即使继续存在,仍然会在该行的末尾被摧毁"Hello world"。对于您的特定目标,您也需要"Hello"

在这一点上,你能说我以前对这个问题感到沮丧吗?

我发现有两种方法是一致的。

  • 通过复制和移动管理您的树,以便最终的模板化表达式真正包含对象(这是您不想要的答案。抱歉)
  • 通过巧妙的引用来管理你的树以避免复制。然后通过删除构造函数来分配一个局部变量来保持它是不可能的。然后仅在表达式中使用操作数(这是您不想要的另一个答案,因为它会导致您提到的 lambda 技巧)。
    • 并且知道您可以将值分配给新的本地引用的人完全破坏了它(因为非根临时节点消失了)。不过,也许这是可以接受的。那些人知道他们是谁,他们应该因为试图变得特别而遇到所有麻烦(不是说我是所说的人之一......)

我自己已经完成了这两种方法。我制作了一个带有临时变量的 JSON 引擎,当使用一半像样的 g++ 或 Visual Studio 编译时,它实际上编译到了创建我的数据结构所需的堆栈中的最小预编译存储数量。这是光荣的(而且几乎没有错误......)。而且我已经构建了无聊的“只是复制数据”结构。

我发现了什么?以我的经验,这种恶作剧得到回报的角落非常小,你需要:

  • 这些构造函数和析构函数的成本并非微不足道的超高性能情况。
  • 表达式树结构首先是有原因的,比如 DAG 转换,这不能用简单的 lambda 来完成
  • 调用这个库的代码需要看起来很干净,以至于你不能为叶节点分配你自己的局部变量(因此回避了唯一真正不可动摇的情况,即你不能在最后一刻复制东西)。
  • 你不能依赖优化器来优化你的代码。

通常这三种情况之一给出。特别是,我注意到 STL 和 Boost 都倾向于采用完全复制的方法。STL 函数默认复制,并std::ref在您想要让自己陷入粗略情况以换取性能时提供。Boost 有很多这样的表达式树。据我所知,他们都依赖于全部复制。我知道 Boost.Phoenix 确实如此(Phoenix 基本上是您原始示例的完整版本),而 Boost.Spirit 也是如此。

这两个示例都遵循我认为您必须遵循的模式:根节点“拥有”它的后代,或者在编译时使用具有操作数作为成员变量的巧妙模板(而不是对所述操作数的引用,a.la. Phoenix),或在运行时(使用指针和堆分配)。

此外,请考虑您的代码变得严格依赖于完全符合规范的 C++ 编译器。尽管比我更好的编译器开发人员尽了最大的努力,但我认为这些实际上并不存在。你生活在一个小角落里,“但它符合规范”可能会被“但我无法编译它”驳斥任何现代编译器!”

我喜欢创意。如果你知道如何做你想做的事,请大声评论我的回答,这样我就可以从你的聪明中学习。但是从我自己努力挖掘 C++ 规范以找到完全符合您要求的方法,我很确定它不存在。

于 2021-07-19T04:59:28.483 回答