【问题标题】:Aggregate reference member and temporary lifetime聚合引用成员和临时生命周期
【发布时间】:2016-05-20 16:36:37
【问题描述】:

鉴于此代码示例,关于传递给 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 中的哪一个具有有效引用?

【问题讨论】:

  • 聚合初始化的临时对象的生命周期延长也不例外,因此临时对象的生命周期将得到延长。这保证了在“直接初始化”案例中创建的临时对象有适当的生命周期。
  • 你的意思是“移动可能会被忽略”?引用绑定不能省略,str_ 直接绑定到other.str。 (std::move 无效)
  • @M.M 我的意思是大多数编译器将执行直接初始化,而不是使用移动构造函数。是的std::move(other).strother.str 相同,用于引用,此处无效
  • 由于CWG 1696,它是 C++14 后的(状态:DRWP),导致绑定临时对象到 mem-initializers 中的引用成员格式错误的更改。其实现状态在clang is "unknown"。不确定 gcc 是否存在这样的列表。

标签: c++ reference initialization aggregate temporary


【解决方案1】:

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

来自最近的草稿,N4567:

第二个上下文[生命周期延长的地方]是当一个 引用绑定到一个临时的。临时的 引用被绑定或者是一个完整对象的临时对象 引用绑定到的子对象在 参考除外:

  • (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"} 使用聚合初始化,因此不涉及内存初始化器。生命周期延长的例外情况均不适用,因此该临时的生命周期将延长至引用数据成员 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),但这是不可观察的,因为它只复制引用。

【讨论】:

  • 也许auto b = S{"bar"}; 会为 C++17 改变? prvalues 更改的目的是 S s{x}; 应该与 auto s = S{x}; 完全相同,尽管我还没有检查过涵盖这种情况的确切措辞。
  • @MM 新的写法对我来说不是很容易理解(“初始化表达式用于初始化目标对象”,17.6.1)但是wg21.link/cwg1697建议bS{"bar"} 引用同一个对象,因此临时的生命周期应该延长到b 的生命周期。不过,我认为编译器目前还没有实现这一点(参见 Live demo of answer)。
猜你喜欢
  • 2016-12-31
  • 2020-01-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多