【问题标题】:Why does extending the lifespan of a temporary object not result in the extension of the intermediate temporaries?为什么延长临时对象的寿命不会导致中间临时对象的延长?
【发布时间】:2017-11-18 22:53:45
【问题描述】:

在 C++ 中,我们可以创建临时值,而这些临时值是有生命周期的。

来自cppreference.com

所有临时对象都作为评估完整表达式的最后一步被销毁,该完整表达式(词法上)包含创建它们的点,如果创建了多个临时对象,它们将按照与创建顺序相反的顺序被销毁. ...

...

  • 可以通过绑定到 const 左值引用或右值引用(C++11 起)来延长临时对象的生命周期,有关详细信息,请参阅引用初始化。

可以编写一个表达式,使得结果对象将具有相关的右值引用。可以通过分配一个非引用对象并将临时对象的内容移动到其中来删除这些依赖项,但由于额外的移动/副本,这将比仅使用临时对象效率低。

通过插入这样一个带有依赖临时对象的表达式作为函数参数,这将导致函数接收一个有效的对象。这是因为表达式已成为完整表达式的子表达式。

但是,如果要延长由同一表达式创建的对象的寿命,则表达式现在已成为完整表达式,因此我希望临时人员在最坏的情况下与最终临时人员一起生活在最好的情况下,或者只是依赖的。然而,似乎所有的中间临时对象都被销毁了,导致临时对象的寿命延长,并带有悬空的引用/指针。

我相信这个问题会变得更加重要,因为我们可以使用右值引用,而不仅仅是 const 引用。

所以我的问题是,为什么会这样?没有用例来延长依赖右值的寿命吗?还是这背后有深思熟虑?

这是我的意思的一个例子:

#include <iostream>

struct Y
{
    Y()  { std::cout << " Y construct\n"; }
    ~Y() { std::cout << " Y destruct\n";  }
};

struct X
{
    Y&& y;
    X(Y&& y)
        : y( (std::cout << " X construct\n",
              std::move(y)) ) {}
    ~X() { std::cout << " X destruct\n"; }
    operator Y&() { return y; }
};

void use(Y& y)
{
    std::cout << " use\n";
}

int main()
{
    std::cout << "used within fn call\n";
    use(X(Y()));
    std::cout << "\nused via life extention\n";
    auto&& x = X(Y());
    use(x);
}

输出:

在 fn 调用中使用
 Y构造
 X 构造
 采用
 X 破坏
 Y破坏

通过寿命延长使用
 Y构造
 X 构造
 Y破坏
 采用
 X 破坏

Demo

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    生命周期延长规则旨在:

    • 防止临时对象超出创建它们的范围;
    • 允许编译器静态确定何时发生生命周期延长以及延长的生命周期何时结束。

    如果不是这样,生命周期延长将会产生运行时成本,其中指针或引用的每次初始化都必须增加与临时对象关联的引用计数。如果这是您想要的,请使用std::shared_ptr

    在您的示例中,X::X(Y&amp;&amp;) 构造函数可以在另一个翻译单元中定义,因此编译器甚至可能无法在翻译时告诉它存储对传入临时变量的引用. 程序不应该根据函数是在这个翻译单元还是在另一个翻译单元中定义而表现不同。即使编译器可以看到X::X 的定义,原则上X::y 的初始化程序可能是一个任意复杂的表达式,它可能会或可能不会实际导致xvalue 引用与参数y 相同的对象。尝试决定可能无法决定的决策问题不是编译器的工作,即使在人类显而易见的特殊情况下也是如此。

    【讨论】:

    • 好的,所以你说的是我所说的最好的情况是不可能的(除非这是内联代码),我同意。然而,最坏的情况呢?为什么不直接将完整表达式的所有中间临时对象的寿命延长到最终延长临时对象的寿命?没有什么可以防止这种情况发生,也不会产生任何运行时开销。
    • @Adrian 如果你的意思是在词汇上属于同一个完整表达式的临时词,当然,这是语言可以做到的,但对于那些不需要的人来说,它会占用额外的内存它,并且还会改变可观察的行为,因为析构函数可能有副作用。
    • 是的,我想是的。我想一种解决方法是使用另一个级别的间接,通过将其传递给函数或 lambda,因此它成为完整表达式的一部分。
    • @Adrian 如果您希望Y() 的生命周期延长,只需将Y&amp;&amp; y = Y(); 放在前面的语句中,然后使用y 作为后续语句的一部分,例如X&amp;&amp; x = X(std::move(y));
    • 这也是另一种可能性,但并非在所有情况下都能正常工作。我正在研究一个转换代理,其中临时由构造函数、初始成员转换器和可能的成员最终转换器创建。所有这些通常在一行中完成。是的,它可以被分解,但是这种解耦会引入更多的错误点。我猜这只是给予和接受。
    猜你喜欢
    • 2016-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-07
    • 1970-01-01
    • 2021-02-05
    相关资源
    最近更新 更多