我无法理解为什么当有完美的转发构造函数时,限制const参考参数的临时值的生命周期被缩短。首先,我们对临时引用参数的临时值的了解:它们持续完整表达式:
函数调用(5.2.2)中与引用参数的临时绑定一直持续到包含调用的完整表达式完成为止
但是我发现这种情况并非如此(或者我可能只是误解了完整表达式是什么)。让我们举一个简单的例子,首先我们用详细的构造函数和析构函数定义一个对象:
struct A {
A(int &&) { cout << "create A" << endl; }
A(A&&) { cout << "move A" << endl; }
~A(){ cout << "kill A" << endl; }
};
一个对象包装器B,它将用于参考折叠:
template <class T> struct B {
T value;
B() : value() { cout << "new B" << endl; }
B(const T &__a) : value(__a) { cout << "create B" << endl; }
B(const B &p) = default;
B(B && o) = default;
~B(){ cout << "kill B" << endl; };
};
我们现在可以使用我们的包装器捕获临时对象的引用并在函数调用中使用它们,如下所示:
void foo(B<const A&> a){ cout << "Using A" << endl; }
int main(){ foo( {123} ); }
上面的程序打印出我期望的内容:
create A
create B
Using A
kill B
kill A
到目前为止一切顺利。现在让我们回到B
并为可转换类型添加一个完美的转发构造函数:
template <class T> struct B {
/* ... */
template <class U, class = typename enable_if<is_convertible<U, T>::value>::type>
B(U &&v) : value(std::forward<U>(v)) {
cout << "new forward initialized B" << endl;
}
};
现在再次编译相同的代码:
create A
new forward initialized B
kill A
Using A
kill B
请注意,我们的A
对象在使用之前就被杀死了,这很糟糕!在这种情况下,为什么临时不的生命周期延伸到foo
的完整调用?此外,没有其他调用A
的析构函数,因此没有其他实例。
我可以看到两种可能的解释:
B(T &&v)
而不是template <class U>B(U &&v)
可以解决问题。{123}
不是foo( {123} )
的子表达式。为{123}
交换A(123)
也解决了这个问题,这让我想知道大括号初始化器是否是完整的表达式。有人可以澄清这里发生了什么吗?
这是否意味着在某些情况下向类中添加转发构造函数会破坏向后兼容性,就像它对B
所做的那样?
您可以找到完整的代码here,其他测试用例会因为对字符串的引用而崩溃。
答案 0 :(得分:5)
U
调用中B<A const&>::B(U&&)
推断的类型为int
,因此foo
中对main
的调用唯一可以延长生命周期的临时值1}}是初始化为int
的prvalue 123
临时值。
成员A const& value
绑定到临时A
,但A
是在构造函数{{1}的 mem-initializer-list 中创建的所以它的生命周期只在成员初始化期间延长 [class.temporary] / 5:
- 构造函数的 ctor-initializer (12.6.2)中与引用成员的临时绑定一直存在,直到构造函数退出。
请注意, mem-initializer-list 是 ctor-initializer 中冒号之后的部分:
B<A const&>::B(U&&)
这就是 template <class U, class = typename enable_if<is_convertible<U, T>::value>::type>
B(U &&v) : value(std::forward<U>(v)) {
^--- ctor-initializer
^--- reference member
^--- temporary A
在 kill A
之后打印的原因。
这是否意味着在某些情况下向类中添加转发构造函数会破坏向后兼容性,就像它对
new forward initialized B
所做的那样?
是。在这种情况下,很难理解为什么转发构造函数是必要的;如果您有一个临时成员可以绑定的参考成员,那肯定是危险的。
答案 1 :(得分:3)
void foo(B<const A&> b);
foo( {123} );
在语义上等同于:
B<const A&> b = {123};
非显式构造函数在语义上等同于:
B<const A&> b{123};
更进一步,因为你的 forwarding-constructor 采取了任何措施,它实际上是用int
初始化的,而不是A
:
B<const A&>::B(int&& v)
也就是说,在构造函数的初始化列表中创建了A
的临时实例:
B(int&& v) : value(A{v}) {}
// created here ^ ^ destroyed here
legal ,就像您可以输入const A& a{123};
。
在A
构造完成后,此B
实例将被销毁,并且您最终会在{{1}的主体内找到悬空引用 }}
当您在调用表达式中构建实例时,情况会发生变化,然后foo
临时在调用表达式结束时结束其生命周期:
A
因此它在 foo( A{123} );
// ^ A is destroyed here
内保持活跃状态,为foo
选择的 forwarding-constructor 将使用类型B<const A&>
进行实例化。