【问题标题】:Extending the lifetime of a temporary object without copying it延长临时对象的生命周期而不复制它
【发布时间】:2020-07-05 01:13:08
【问题描述】:

考虑以下代码:

#include <utility>
#include <iostream>

struct object {
    object(const object&) = delete;
    object(object&&) = delete;
    object() {std::clog << "object::object()\n";}
    ~object() {std::clog << "object::~object()\n";}
    void operator()() const {std::clog << "object::operator()()\n";}
};

struct wrapper {
    const object& reference;
    void operator()() const {reference();}
};

template <class Arg>
wrapper function(Arg&& arg) {
    wrapper wrap{std::forward<Arg>(arg)};
    return wrap;
}

int main(int argc, char* argv[]) {
    wrapper wrap = function(object{}); // Let's call that temporary object x
    wrap();
    return 0;
}

我真的很惊讶它会打印出来:

object::object()
object::~object()
object::operator()()

问题 1:为什么对象 x 的生命周期没有超过函数调用,即使绑定了 const 引用?

问题 2: 有什么方法可以实现 wrapper,以便在函数调用之后延长 x 的生命周期?

注意:object 的复制和移动构造函数已被显式删除,以确保它只存在一个实例。

【问题讨论】:

标签: c++ lifetime rvalue-reference temporary object-lifetime


【解决方案1】:

为什么对象 x 的生命周期没有延长到函数调用之后,即使已经绑定了 const 引用?

从技术上讲,对象的生命周期会延长到函数调用之后。然而,它并没有扩展到wrap 的初始化之后。但这是技术性问题。

在我们深入研究之前,我要进行简化:让我们摆脱@​​987654322@。另外,我删除了模板部分,因为它也无关紧要:

const object &function(const object &arg)
{
  return arg;
}

这完全不会改变您代码的有效性。

鉴于此声明:

const object &obj = function(object{}); // Let's call that temporary object x

您想要编译器识别“object x”和obj 指的是同一个对象,因此应该延长临时对象的生命周期。

这是不可能的。不保证编译器有足够的信息来知道这一点。为什么?因为编译器可能只知道这一点:

const object &function(const object &arg);

看,function定义arg 与返回值相关联。如果编译器没有function 的定义,那么它无法知道传入的对象是返回的引用。没有这些知识,它就无法知道延长x 的生命周期。

现在,你可能会说,如果提供了function 的定义,那么编译器就可以知道。嗯,有复杂的逻辑链可能会阻止编译器在编译时知道。你可以这样做:

const object *minimum(const object &lhs, const object &rhs)
{
  return lhs < rhs ? lhs : rhs;
}

嗯,这会返回对其中一个的引用,但哪一个只能根据对象的运行时值来确定。调用者应该延长谁的生命周期?

我们也不希望代码的行为根据编译器只有声明还是完整定义而改变。要么总是可以编译只有声明的代码,要么从不只编译带有声明的代码(如inline的情况) 、constexprtemplate 函数)。声明可能会影响性能,但绝不会行为。这很好。

由于编译器可能没有识别参数const&amp; 超出函数生命周期所需的信息,即使它有这些信息,它也可能不是可以静态确定的东西,C++ 标准确实不允许实现甚至尝试来解决问题。因此,每个 C++ 用户都必须认识到,如果返回引用,则在临时对象上调用函数可能会导致问题。即使引用隐藏在其他对象中。

你想做的事是做不到的。这就是为什么你不应该让一个对象完全不可移动的原因之一,除非它对它的行为或性能是必不可少的。

【讨论】:

    【解决方案2】:

    据我所知,如果为函数的返回值延长生命周期的唯一情况,

    struct A { int a; };
    A f() { A a { 42 }; return a`}
    {
        const A &r = f(); // take a reference to a object returned by value
        ...
        // life or r extended to the end of this scope
    }
    

    在您的代码中,您将引用传递给 A 类的“构造函数”。因此,您有责任确保传递的对象寿命更长。因此,您上面的代码包含未定义的行为

    你看到的可能是类中最可能的行为,它不引用任何成员。如果您要访问对象成员(包括 v-table),您很可能会观察到违规访问。

    话虽如此,您案例中的正确代码是:

    int main(int argc, char* argv[]) 
    {
        object obj {};
        wrapper wrap = function(obj);
        wrap();
        return 0;
    }
    

    也许你想要的是将临时对象移动到包装器中:

    struct wrapper {
        wrapper(object &&o) : obj(std::move(o)) {}
        object obj;
        void operator()() const {obj();}
    };
    

    无论如何,原始代码没有多大意义,因为它是围绕错误假设构建的并且包含未定义行为

    临时对象的生命本质上是创建它的表达式的结束。即当处理wrap = function(object{})完成时。

    所以在简历中:

    答案 1 因为您尝试将生命周期延长应用于标准中指定的上下文之外的上下文。

    答案 2 就像将临时对象移动到永久对象一样简单。

    【讨论】:

      猜你喜欢
      • 2013-11-20
      • 2019-01-06
      • 1970-01-01
      • 1970-01-01
      • 2012-11-15
      • 1970-01-01
      • 2023-02-03
      • 1970-01-01
      相关资源
      最近更新 更多