聚合参考成员和临时生命周期

Aggregate reference member and temporary lifetime

给定此代码示例,有关传递给 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 中删除)

A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

N4296 12.6.2 p8

A temporary expression bound to a reference member in a mem-initializer is ill-formed.

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

关于复制初始化, 指出 [2] 是隐式生成的。鉴于移动可能被省略的事实,相同的规则是否适用于隐式生成的移动构造函数?

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

绑定到引用的临时对象的生命周期被延长,除非有特定的例外。即如果没有异常,那么生命周期会延长。

来自最近的草稿,N4567:

The second context [where the lifetime is extended] is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

  • (5.1) A temporary object bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.
  • (5.2) The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
  • (5.3) A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression containing the new-initializer.

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

  • A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

这已在 CWG 1696 (post-C++14) 中删除,现在通过内存初始化器将临时对象绑定到引用数据成员的格式不正确。


关于 OP 中的示例:

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

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

这将创建一个临时 std::string 并用它初始化 str_ 数据成员。 S a{"foo"} 使用聚合初始化,因此不涉及内存初始化器。 None 生命周期延长的例外情况适用,因此该临时数据成员的生命周期延长至引用数据成员的生命周期 str_


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

在使用 C++17 进行强制复制省略之前: 形式上,我们创建一个临时std::string,通过将临时std::string绑定到str_引用成员来初始化一个临时S。然后,我们将临时 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 结束”之前被调用,而 x 一直存在到该跟踪之后。正式地,临时 loud 在创建它的完整表达式的末尾被销毁。

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

使用 C++17 中的强制复制省略: 我们将右手 S{"bar"} 上的对象与左手 b 上的对象标识在一起。这导致临时的生命周期延长到 b 的生命周期。参见 CWG 1697


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