通过rvalue数据成员延长临时生命周期与聚合一起使用,但不使用构造函数,为什么?

Rya*_*ing 12 c++ object-lifetime rvalue-reference language-lawyer c++11

我发现以下方案延长了临时工作的寿命,我不知道是否应该,但确实如此.

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
}
Run Code Online (Sandbox Code Playgroud)

但是,当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
}
Run Code Online (Sandbox Code Playgroud)

为什么这对聚合有效?我认为它与构造函数是一个实际的函数调用有关,基于我使用const lvalue refs的红色.另外,有没有办法让后一种情况起作用?

在SO上使用左值引用处理类似情况有很多问题.我看到如果我使用了const lvalue ref,那么延长临时值的生命周期就无济于事了,rvalue refs的规则是否相同?

Fil*_*efp 10

TL; DR

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

:两个T const&T&&在的情况下申请聚集型initalization并延长绑定到这些临时工的生活.



什么是聚合

struct S {                // (1)
  std::vector<int>&& vec;
};
Run Code Online (Sandbox Code Playgroud)

要回答这个问题,我们将不得不深入研究聚合的 初始化和类类型的初始化之间的区别,但首先我们必须确定聚合是什么:

8.5.1p1 骨料 [dcl.init.aggr]

骨料是没有用户提供的构造的阵列或一个类(第9节)(12.1),无私有或保护非静态数据成员(第11),没有基类(第10节),并且没有虚拟功能(10.3 )

注意:以上意味着(1)是一个聚合.



聚合如何初始化?

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

8.5.1p2 骨料 [dcl.init.aggr]

当初始化程序列表初始化聚合时,如8.5.4 中所述,初始化程序列表的元素将作为聚合成员的初始化程序,增加下标或成员顺序.每个成员都是从相应的initializer子句复制初始化的.


上面的引用声明我们正在使用initializer-clause中的初始化器初始化聚合的成员,两者之间没有任何步骤.

struct A { std::string a; int b; };
Run Code Online (Sandbox Code Playgroud)

A x { std::string {"abc"}, 2 };
Run Code Online (Sandbox Code Playgroud)


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

std::string A::a { std::string {"abc"} };
int         A::b { 2 };
Run Code Online (Sandbox Code Playgroud)


如果我们将类型更改为A::arvalue-reference或const lvalue-reference,我们将直接绑定临时用于初始化x.a.

rvalue-referencesconst lvalue-references的规则表明temporaries的生命周期将扩展到主机的生命周期,这正是将要发生的事情.



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

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

一个构造器是真的没有什么比一个奇特的功能,用来初始化更多的类的实例.适用于函数的相同规则适用于它们.

当谈到延长临时生命的时间时,没有任何区别.

std::string&& func (std::string&& ref) {
  return std::move (ref);
}
Run Code Online (Sandbox Code Playgroud)


临时传递给func它的生命期不会因为我们有一个被声明为rvalue/lvalue-reference的参数而延长.即使我们返回"相同"的引用以便它在外面可用func,它也不会发生.

这是在(2)的构造函数中发生的事情,毕竟构造函数只是用于初始化对象的" 奇特函数 ".


12.2p5 临时对象 [class.temporary]

绑定引用的临时对象或绑定引用的子对象的完整对象的临时对象在引用的生命周期内持续存在,除了:

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

  • 函数调用(5.2.2)中的引用参数的临时绑定将持续到包含该调用的完整表达式完成为止.

  • 函数返回语句(6.6.3)中返回值临时绑定的生命周期未扩展; 临时在return语句中的full-expression结束时被销毁.

    • new-initializer(5.3.4)中对引用的临时绑定将持续到包含new-initializer的full-expression完成为止.

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