Given this code sample, what are the rules regarding the lifetime of the temporary string being passed to S
.
给定这个代码示例,传递给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
According to the standard:
根据标准:
N4140 12.2 p5.1 (removed in N4296)
n414012.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.
一个临时绑定到构造函数的键-初始化器(12.6.2)中的引用成员,直到构造函数退出为止。
N4296 12.6.2 p8
N4296 12.6.2 p8
A temporary expression bound to a reference member in a mem-initializer is ill-formed.
绑定到内存初始化器中的引用成员的临时表达式是病态的。
So having a user defined constructor like [1]
is definitively not what we want. It's even supposed to be ill-formed in the latest C++14 (or is it?) neither gcc nor clang warned about it.
Does it change with direct aggregate initialization? I looks like in that case, the temporary lifetime is extended.
因此,像[1]这样的用户定义构造函数肯定不是我们想要的。在最新的c++ 14(或者是?)中,它甚至被认为是不正确的,gcc和clang都没有发出警告。它会随着直接聚合初始化而改变吗?在这种情况下,临时生命期延长了。
Now regarding copy-initialization, Default move constructor and reference members states that [2]
is implicitly generated. Given the fact that the move might be elided, does the same rule apply to the implicitly generated move constructor?
现在关于复制初始化,默认的move构造函数和引用成员声明[2]是隐式生成的。考虑到移动可能被省略,同样的规则是否适用于隐式生成的move构造函数?
Which of a, b, c, d
has a valid reference?
哪一个a b c d有有效的引用?
1 个解决方案
#1
3
The lifetime of temporary objects bound to references is extended, unless there's a specific exception. That is, if there is no such exception, then the lifetime will be extended.
除非有特定的异常,否则绑定到引用的临时对象的生命周期将被延长。也就是说,如果没有这样的例外,那么生命期将被延长。
From a fairly recent draft, N4567:
从最近的草稿来看,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.1)函数调用中绑定到引用参数的临时对象(5.2.2)将持续到包含调用的完整表达式完成为止。
- (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.2)函数返回语句(6.6.3)中临时绑定到返回值的生命周期不被扩展;临时语句在返回语句的全表达式末尾被销毁。
- (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.
- (5.3)在新初始化器(5.3.4)中对引用的临时绑定,直到包含新初始化器的完整表达式的完成为止。
The only significant change to C++11 is, as the OP mentioned, that in C++11 there was an additional exception for data members of reference types (from N3337):
对于c++ 11唯一显著的改变是,正如OP提到的,在c++ 11中,引用类型的数据成员有一个额外的例外(来自N3337):
- A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.
- 一个临时绑定到构造函数的键-初始化器(12.6.2)中的引用成员,直到构造函数退出为止。
This was removed in CWG 1696 (post-C++14), and binding temporary objects to reference data members via the mem-initializer is now ill-formed.
在CWG 1696(后c++ 14)中删除了这一点,并且通过内存初始化器将临时对象绑定到引用数据成员现在是病态的。
Regarding the examples in the OP:
关于OP中的例子:
struct S { const std::string& str_; }; S a{"foo"}; // direct-initialization
This creates a temporary std::string
and initializes the str_
data member with it. The S a{"foo"}
uses aggregate-initialization, so no mem-initializer is involved. None of the exceptions for lifetime extensions apply, therefore the lifetime of that temporary is extended to the lifetime of the reference data member str_
.
这将创建一个临时的std::string并使用它初始化str_数据成员。S a{“foo”}使用聚合初始化,因此不涉及任何内存初始化。生命周期扩展的所有异常都不适用,因此该临时扩展的生命周期被扩展到引用数据成员str_的生命周期。
auto b = S{"bar"}; // copy-initialization with rvalue
Prior to mandatory copy elision with C++17: Formally, we create a temporary std::string
, initialize a temporary S
by binding the temporary std::string
to the str_
reference member. Then, we move that temporary S
into b
. This will "copy" the reference, which will not extend the lifetime of the std::string
temporary. However, implementations will elide the move from the temporary S
to b
. This must not affect the lifetime of the temporary std::string
though. You can observe this in the following program:
在强制复制elision与c++ 17之前:形式上,我们创建一个临时std::string,通过绑定临时std::string to the str_ reference member来初始化临时的S。然后,我们将临时的S移动到b中。这将“复制”引用,不会延长std::string temporary的生存期。但是,实现将避免从临时的S转移到b。这一定不会影响临时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;
}
现场演示
Note that the destructor of loud
is called before the "end of main", whereas x
lives until after that trace. Formally, the temporary loud
is destroyed at the end of the full-expression which created it.
注意,loud的析构函数在"end of main"之前被调用,而x在该跟踪之后才被调用。正式地说,暂时的喧闹声在创造它的完整表达的结尾被破坏了。
The behaviour does not change if the move constructor of aggr
is user-defined.
如果aggr的move构造函数是用户定义的,则该行为不会更改。
With mandatory copy-elision in C++17: We identify the object on the rhs S{"bar"}
with the object on the lhs b
. This causes the lifetime of the temporary to be extended to the lifetime of b
. See CWG 1697.
C+ 17中的强制复制省略:我们将rhs上的对象与lhs b上的对象标识,这将使临时对象的生存期延长到b的生存期,参见CWG 1697。
For the remaining two examples, the move constructor - if called - simply copies the reference. The move constructor (of S
) can be elided, of course, but this is not observable since it only copies the reference.
对于剩下的两个示例,move构造函数(如果调用)只复制引用。当然,可以省略move构造函数(S),但这是不可观察的,因为它只复制引用。
#1
3
The lifetime of temporary objects bound to references is extended, unless there's a specific exception. That is, if there is no such exception, then the lifetime will be extended.
除非有特定的异常,否则绑定到引用的临时对象的生命周期将被延长。也就是说,如果没有这样的例外,那么生命期将被延长。
From a fairly recent draft, N4567:
从最近的草稿来看,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.1)函数调用中绑定到引用参数的临时对象(5.2.2)将持续到包含调用的完整表达式完成为止。
- (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.2)函数返回语句(6.6.3)中临时绑定到返回值的生命周期不被扩展;临时语句在返回语句的全表达式末尾被销毁。
- (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.
- (5.3)在新初始化器(5.3.4)中对引用的临时绑定,直到包含新初始化器的完整表达式的完成为止。
The only significant change to C++11 is, as the OP mentioned, that in C++11 there was an additional exception for data members of reference types (from N3337):
对于c++ 11唯一显著的改变是,正如OP提到的,在c++ 11中,引用类型的数据成员有一个额外的例外(来自N3337):
- A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.
- 一个临时绑定到构造函数的键-初始化器(12.6.2)中的引用成员,直到构造函数退出为止。
This was removed in CWG 1696 (post-C++14), and binding temporary objects to reference data members via the mem-initializer is now ill-formed.
在CWG 1696(后c++ 14)中删除了这一点,并且通过内存初始化器将临时对象绑定到引用数据成员现在是病态的。
Regarding the examples in the OP:
关于OP中的例子:
struct S { const std::string& str_; }; S a{"foo"}; // direct-initialization
This creates a temporary std::string
and initializes the str_
data member with it. The S a{"foo"}
uses aggregate-initialization, so no mem-initializer is involved. None of the exceptions for lifetime extensions apply, therefore the lifetime of that temporary is extended to the lifetime of the reference data member str_
.
这将创建一个临时的std::string并使用它初始化str_数据成员。S a{“foo”}使用聚合初始化,因此不涉及任何内存初始化。生命周期扩展的所有异常都不适用,因此该临时扩展的生命周期被扩展到引用数据成员str_的生命周期。
auto b = S{"bar"}; // copy-initialization with rvalue
Prior to mandatory copy elision with C++17: Formally, we create a temporary std::string
, initialize a temporary S
by binding the temporary std::string
to the str_
reference member. Then, we move that temporary S
into b
. This will "copy" the reference, which will not extend the lifetime of the std::string
temporary. However, implementations will elide the move from the temporary S
to b
. This must not affect the lifetime of the temporary std::string
though. You can observe this in the following program:
在强制复制elision与c++ 17之前:形式上,我们创建一个临时std::string,通过绑定临时std::string to the str_ reference member来初始化临时的S。然后,我们将临时的S移动到b中。这将“复制”引用,不会延长std::string temporary的生存期。但是,实现将避免从临时的S转移到b。这一定不会影响临时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;
}
现场演示
Note that the destructor of loud
is called before the "end of main", whereas x
lives until after that trace. Formally, the temporary loud
is destroyed at the end of the full-expression which created it.
注意,loud的析构函数在"end of main"之前被调用,而x在该跟踪之后才被调用。正式地说,暂时的喧闹声在创造它的完整表达的结尾被破坏了。
The behaviour does not change if the move constructor of aggr
is user-defined.
如果aggr的move构造函数是用户定义的,则该行为不会更改。
With mandatory copy-elision in C++17: We identify the object on the rhs S{"bar"}
with the object on the lhs b
. This causes the lifetime of the temporary to be extended to the lifetime of b
. See CWG 1697.
C+ 17中的强制复制省略:我们将rhs上的对象与lhs b上的对象标识,这将使临时对象的生存期延长到b的生存期,参见CWG 1697。
For the remaining two examples, the move constructor - if called - simply copies the reference. The move constructor (of S
) can be elided, of course, but this is not observable since it only copies the reference.
对于剩下的两个示例,move构造函数(如果调用)只复制引用。当然,可以省略move构造函数(S),但这是不可观察的,因为它只复制引用。