【问题标题】:Extending temporary's lifetime through rvalue data-member works with aggregate, but not with constructor, why?通过右值数据成员延长临时的生命周期适用于聚合,但不适用于构造函数,为什么?
【发布时间】:2014-07-16 12:28:20
【问题描述】:

我发现以下延长临时人员生命周期的方案有效,我不知道是否应该,但确实如此。

struct S {
    std::vector<int>&& vec;
};

int main() {
    S s1{std::vector<int>(5)};      // construct with temporary
    std::cout << s1.vec[0] << '\n'; // fine, temporary is alive
}

但是,当 S 被赋予显式值构造函数时,它不再是一个聚合,并且该方案因s1.vec[0] 上的无效读取而失败

struct S {
    std::vector<int>&& vec;
    S(std::vector<int>&& v)
        : vec{std::move(v)}         // bind to the temporary provided
    { }
};

int main() {
    S s1{std::vector<int>(5)};      // construct with temporary
    std::cout << s1.vec[0] << '\n'; // not ok. invalid read on free'd memory
}

为什么这对聚合有效?根据我使用 const lvalue refs 阅读的内容,我认为它与构造函数是一个实际的函数调用有关。另外,有什么办法可以使后一种情况起作用吗?

在 SO 上使用左值引用处理类似情况有很多问题。我看到如果我使用了 const 左值引用,它不会有助于延长临时值的生命周期,右值引用的规则是否相同?

【问题讨论】:

  • "const lvalue refs 不能延长临时对象的生命周期" - 嗯?
  • 这看起来像是对 C++ 的滥用(它有效并不意味着它是正确的,它可能是未定义的行为)
  • 临时对象的生命周期延长规则与将临时对象绑定到引用有关,它们不区分左值和右值引用。
  • 第一句话的说法有问题,提供的测试太简单了。
  • 如果我没记错的话,标准中有一些有趣的条款使这种情况与其他情况不同。就像“构造函数中的引用绑定不会延长生命周期”。这里缺少构造函数意味着引用绑定是由编译器直接完成的,所以......它可能会起作用。当我考虑提出“生命周期链接”以在通过辅助函数转发临时对象时绕过一些不必要的副本时,我研究了生命周期扩展规则。

标签: c++ c++11 language-lawyer rvalue-reference object-lifetime


【解决方案1】:

TL;DR

聚合初始化可用于延长临时的生命周期,用户定义的构造函数不能这样做,因为它实际上是一个函数调用。 p>

注意T const&amp;T&amp;&amp; 都适用于 聚合初始化 并延长绑定到它们的临时对象的寿命。子>



什么是聚合

struct S {                // (1)
  std::vector<int>&& vec;
};

要回答这个问题,我们必须深入了解聚合的初始化和类类型的初始化之间的区别,但首先我们必须确定聚合是:

8.5.1p1 聚合 [dcl.init.aggr]

聚合是一个数组或类(第 9 条),没有用户提供的构造函数(12.1),没有私有或受保护的非静态数据成员(第 11 条),没有基类(第 10 条),并且没有虚函数(10.3)

注意:以上表示(1)是一个聚合体。



聚合是如何初始化的?

聚合和“非聚合”之间的初始化有很大的不同,这里有另一节直接来自标准:

8.5.1p2 聚合 [dcl.init.aggr]

当聚合被初始化列表初始化时,如 8.5.4 中所指定,初始化列表的元素被视为聚合成员的初始化,以递增的下标或成员顺序.每个成员都从相应的initializer-clause复制初始化


上面的引用表明我们正在用 initializer-clause 中的初始化器初始化我们的 聚合 的成员,中间没有步骤。

struct A { std::string a; int b; };

A x { std::string {"abc"}, 2 };


在语义上,上面相当于使用下面来初始化我们的成员,只是在这种情况下A::aA::b只能通过x.ax.b访问。

std::string A::a { std::string {"abc"} };
int         A::b { 2 };


如果我们将A::a的类型更改为右值引用,或者const lvalue-reference,我们将直接将初始化的临时使用绑定到@ 987654336@.

rvalue-referencesconst lvalue-references 的规则说临时对象的生命周期将延长到主机的生命周期,这正是正在发生的事情发生。



使用用户声明的构造函数进行初始化有何不同?

struct S {                    // (2)
    std::vector<int>&& vec;
    S(std::vector<int>&& v)
        : vec{std::move(v)}   // bind to the temporary provided
    { }
};

constructor 实际上只不过是一个花哨的函数,用于初始化 class 实例。适用于函数的相同规则也适用于它们。

在延长临时工的使用寿命方面没有区别。

std::string&& func (std::string&& ref) {
  return std::move (ref);
}


传递给func 的临时变量不会因为我们将参数声明为右值/左值引用而延长其生命周期。即使我们返回 "same" 引用以使其在 func 之外可用,它也不会发生。

这就是(2)的构造函数中发生的事情,毕竟构造函数只是一个用来初始化的“花式函数”一个对象。


12.2p5 临时对象 [class.temporary]

引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生命周期内持续存在,但以下情况除外:

  • 临时绑定到构造函数的 ctor-initializer (12.6.2) 中的引用成员会一直存在,直到构造函数退出。

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

  • 在函数返回语句 (6.6.3) 中临时绑定到返回值的生命周期没有延长;临时在 return 语句中的完整表达式的末尾被销毁。

    • new-initializer (5.3.4) 中的引用的临时绑定将持续存在,直到包含 new-initializer 的完整表达式完成。李>

注意:请注意,通过new T { ... } 进行的聚合初始化与前面提到的规则不同。

【讨论】:

  • 这一切都很有道理,谢谢。如果我正确阅读了您的最后一条注释,如果您通过 new 使用聚合初始化,则临时对象的生命周期不会延长(我假设是因为对象是非确定性分配的)对吗?
  • @RyanHaining 是正确的。我正在考虑在更详细的Q&A 中回答为什么会这样。有兴趣吗?
  • 我认为这与编译器不知道临时生命周期何时结束有关。如果一个非常量的 news 具有相同的右值 ref,则该右值的生命周期现在将不确定地延长。所以这意味着2个选项,一个是将右值引用移动到new'd对象中,但是这两个位置的右值地址会不同,更不用说编译器不一定是移动构造函数允许插入。另一种是将它留在堆栈上,但 newd 对象可能会比临时对象寿命更长。
  • 但是,我可能在这方面还不够,如果你要详细说明,那么我会感兴趣的。
  • @Frunsi 我在这里谈论的是新分配与非新分配。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-31
  • 2016-05-20
  • 2022-01-22
  • 2022-10-13
  • 2012-11-15
  • 1970-01-01
相关资源
最近更新 更多