保存生命的最佳方式..来自C++表达式的临时变量



考虑一个带有重载右关联C++运算符的二进制运算Xa+=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内部访问其成员。。。但你不能(在外面)重复使用它!你每次都必须重新创造它(有人把这种方法称为"厄运的羔羊",也许还有其他缺点)

这是一个测试程序(位于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} %sn", this, &this->l, &this->r, __FUNCSIG__);};
~X(){printf("X{this=%p} %sn", 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} %sn", this, &this->l, &this->r, __FUNCSIG__);
assert(&this->r == &r);
};
~Y(){printf("Y{this=%p} %sn", this, __FUNCSIG__);};
void func(){printf("Y{this=%p} &r=%p ... ALREADY DELETED! %sn", this, &r, __FUNCSIG__);};
};
struct Operand{
Operand(){printf("Operand{this=%p} %sn", this, __FUNCSIG__);}
~Operand(){printf("Operand{this=%p} %sn", this, __FUNCSIG__);}
};
//================================================================
int main(){
Operand a, b, c;
printf("---- 1 expression with temporariesn");
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("---- 3n");
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!

没有办法按照您的意愿延长临时工的寿命。

有几种方法可以延长临时生命。他们中的大多数人都没有帮助。例如,在构造函数期间初始化成员时使用的临时项将一直持续到构造函数结束。这在一个";帘布层";这样一个表达式树,但对两个表达式树没有帮助。

一个有趣的方式可以延长一个临时的生命是成为一个参考的主题。

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

这种情况将一直持续到x超出范围。但它不会延长其他临时工的寿命。即使"Hello world"还活着,"Hello"仍然会在这条线的尽头被摧毁。为了你的特定目标,你也需要"Hello"

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

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

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

这两种方法我自己都做过。我制作了一个带有临时变量的JSON引擎,当使用一个半像样的g++或Visual Studio进行编译时,它实际上被编译到了创建数据结构所需的堆栈中的最小预编译存储数。它是光荣的(和几乎bug免费…);只需复制数据";结构。

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

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

通常这三种情况中的一种会给出。特别是,我注意到STL和Boost都倾向于采用复制一切的方法。STL函数默认情况下是复制的,当您想让自己陷入粗略的情况以换取性能时,它会提供std::ref。Boost有很多这样的表达式树。据我所知,他们都依赖于全部复制。我知道Boost。Phoenix知道(Phoenix基本上是你最初例子的完整版本),Boost。Spirit也知道。

这两个例子都遵循一种模式,我认为你必须遵循这种模式:根节点";拥有";它的后代,要么在编译时使用聪明的模板,将操作数作为成员变量(而不是引用所述操作数,a.la.Fenix),要么在运行时使用指针和堆分配。

此外,请考虑您的代码变得严格依赖于完全符合规范的C++编译器。尽管比我优秀的编译器开发人员尽了最大努力,但我认为这些实际上并不存在;但它符合规范";可以用";但我不能在任何现代编译器上编译它">

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

最新更新