汇总参考成员和临时生命周期

时间:2016-02-10 10:58:17

标签: c++ reference initialization aggregate temporary

鉴于此代码示例,有关传递给S的临时字符串的生命周期的规则是什么。

struct S
{
    // [1] S(const std::string& str) : str_{str} {}
    // [2] S(S&& other) : str_{std::move(other).str} {}

    const std::string& str_;
};

S a{"foo"}; // direct-initialization

auto b = S{"bar"}; // copy-initialization with rvalue

std::string foobar{"foobar"};
auto c = S{foobar}; // copy-initialization with lvalue

const std::string& baz = "baz";
auto d = S{baz}; // copy-initialization with lvalue-ref to temporary

根据标准:

N4140 12.2 p5.1(在N4296中删除)

  

构造函数的ctor-initializer(12.6.2)中对引用成员的临时绑定一直持续到   构造函数退出。

N4296 12.6.2 p8

  

绑定到mem-initializer中引用成员的临时表达式格式不正确。

因此,拥有像[1]这样的用户定义构造函数绝对不是我们想要的。它甚至应该在最新的C ++ 14中形成不良(或者是它?)gcc和clang都没有警告它。
它是否随直接聚合初始化而改变?在这种情况下,我看起来像是延长了临时寿命。

现在关于复制初始化,Default move constructor and reference members表示隐式生成[2]。鉴于移动可能被省略,同样的规则是否适用于隐式生成的移动构造函数?

a, b, c, d中哪一个有有效参考?

1 个答案:

答案 0 :(得分:5)

除非存在特定异常,否则将扩展绑定到引用的临时对象的生命周期。也就是说,如果没有这样的例外,那么寿命将会延长。

从最近的草案中,N4567:

  

第二个上下文[寿命延长的地方]是a   引用必然是临时的。临时的   引用是绑定的,或者是临时的,是一个完整的对象   引用绑定的子对象在其生命周期内持续存在   引用除外:

     
      
  • (5.1)绑定到函数调用(5.2.2)中的引用参数的临时对象一直持续到完成   包含调用的完整表达式。
  •   
  • (5.2)函数返回语句(6.6.3)中返回值临时绑定的生命周期未扩展;暂时的是   在return语句中的完整表达结束时被销毁。
  •   
  • (5.3)在new-initializer(5.3.4)中对引用的临时绑定一直持续到完整表达式完成   包含new-initializer。
  •   

正如OP所提到的,对C ++ 11的唯一重大改变是,在C ++ 11中,引用类型的数据成员(来自N3337)还有一个例外:

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

这已在CWG 1696(后C ++ 14)中删除,并且通过mem-initializer将临时对象绑定到引用数据成员现在格式不正确。

关于OP中的例子:

struct S
{
    const std::string& str_;
};

S a{"foo"}; // direct-initialization

这会创建一个临时std::string并使用它初始化str_数据成员。 S a{"foo"}使用聚合初始化,因此不涉及mem-initializer。生命周期扩展的例外都不适用,因此该临时值的生命周期延长到参考数据成员str_的生命周期。

auto b = S{"bar"}; // copy-initialization with rvalue

在使用C ++ 17进行强制复制省略之前 在形式上,我们创建一个临时std::string,通过将临时S绑定到std::string引用成员来初始化临时str_。然后,我们将该临时S移至b。这将“复制”引用,这不会延长std::string临时的生命周期。 但是,实施将忽略从临时Sb的移动。但这不得影响临时std::string的生命周期。您可以在以下程序中观察到这一点:

#include <iostream>

#define PRINT_FUNC() { std::cout << __PRETTY_FUNCTION__ << "\n"; }

struct loud
{
    loud() PRINT_FUNC()
    loud(loud const&) PRINT_FUNC()
    loud(loud&&) PRINT_FUNC()
    ~loud() PRINT_FUNC()
};

struct aggr
{
    loud const& l;
    ~aggr() PRINT_FUNC()
};

int main() {
    auto x = aggr{loud{}};
    std::cout << "end of main\n";
    (void)x;
}

Live demo

请注意,loud的析构函数在“main of end”之前调用,而x生效直到该跟踪之后。形式上,临时loud在创建它的完整表达式结束时被销毁。

如果aggr的移动构造函数是用户定义的,则行为不会改变。

在C ++ 17中强制使用copy-elision:我们使用lhs S{"bar"}上的对象识别rhs b上的对象。这会导致临时的生命周期延长到b的生命周期。请参阅CWG 1697

对于剩下的两个例子,移动构造函数 - 如果被调用 - 只是复制引用。当然,移动构造函数(S)可以省略,但这是不可观察的,因为它只复制引用。