返回对绑定到临时的引用参数的引用时的悬空引用
Dangling reference when returning reference to reference parameter bound to temporary
此问题参考了 Howard Hinnant 对问题 的回答。
在他的回答底部,他说:
Note that in this latest design, if your client ever does this:
X&& x = a + b + c;
then x is a dangling reference (which is why std::string does not do this).
cppreference.com 上的“引用初始化”一文的“临时对象的生命周期”段落列出了绑定到引用的临时对象的生命周期规则的例外情况。一个是:
"a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference."
我认为它的意思是“如果函数 returns 引用 引用参数绑定到 的临时对象”,而不仅仅是其他引用。因此,我认为这就是解释 Howard Hinnant 上述陈述的规则。
以下示例基于我所指的问题中给出的示例:
struct X
{
int _x;
X() : _x(0) {}
X(int x) : _x(x) {}
X(X const& other) : _x(other._x) {}
X(X&& other) noexcept : _x(other._x) { other._x = 0; std::cout << "Move from " << &other << " to " << this << std::endl; }
X& operator+=(const X& other) { _x += other._x; return *this; }
friend X operator+(X const& lhs, X const& rhs)
{
std::cout << "X const& lhs: " << &lhs << std::endl;
X temp = lhs;
temp += rhs;
return temp;
}
friend X&& operator+(X&& lhs, X const& rhs)
{
std::cout << "X&& lhs: " << &lhs << std::endl;
lhs += rhs;
return std::move(lhs);
}
};
int anotherFunc(int a)
{
int bigArray[3000]{};
std::cout << "ignore:" << &bigArray << std::endl;
int b = a * a;
std::cout << "int b: " << &b << std::endl;
return 2 * b;
}
int main()
{
X a(1), b(2), c(3), d(4);
X&& sum = a + b + c + d;
std::cout << "X&& sum: " << &sum << std::endl;
anotherFunc(15);
std::cout << "sum._x: " << sum._x << std::endl;
return 0;
}
这会打印
X const& lhs: 000000907DAFF8B4
Move from 000000907DAFF794 to 000000907DAFFA14
X&& lhs: 000000907DAFFA14
X&& lhs: 000000907DAFFA14
X&& sum: 000000907DAFFA14
ignore:000000907DAFC360
int b: 000000907DAFF254
sum._x: 10
使用 MSVC 编译时;以及使用 gcc 或 clang 编译时的类似输出。
sum
应该是这里的悬空引用。尽管如此,仍会打印出正确的值“10”。它甚至可以在 sum
的引用初始化和通过所述引用进行访问之间将大型数组推入堆栈时起作用。用于 sum 引用的临时对象的内存不会被重用,并且总是在别处分配(相对于以下函数调用的堆栈帧),无论下一个堆栈帧有多大或多小。
为什么我测试过的每个编译器都保留 X&& operator+(X&& lhs, X const& rhs)
本地的临时对象,即使 sum
根据 cppreference.com 上的规则应该是一个悬空引用。或者,更准确地说:尽管访问悬垂引用是未定义的行为,但为什么 每个 编译器都以这种方式实现它?
我喜欢为这种情况保留一个示例 class A
。 A
的完整定义有点太长,无法在此处列出,但完整包含 at this link.
简而言之,A
保留了一个 state
和一个 status
,而 status
可以是这些枚举之一:
destructed = -4,
self_move_assigned = -3,
move_assigned_from = -2,
move_constructed_from = -1,
constructed_specified = 0
即特殊会员相应设置状态。例如 ~A()
看起来像这样:
~A()
{
assert(is_valid());
--count;
state_ = randomize();
status_ = destructed;
}
并且有一个流媒体运营商打印这个class。
语言律师免责声明:打印出一个破坏的A
是未定义的行为,任何事情都可能发生。话虽这么说,当在优化 关闭 的情况下编译实验时,您通常会得到预期的结果。
对我来说,在 -O0
使用 clang
,这个:
#include "A.h"
#include <iostream>
int
main()
{
A a{1};
A b{2};
A c{3};
A&& x = a + b + c;
std::cout << x << '\n';
}
输出:
destructed: -1002199219
将行更改为:
A x = a + b + c;
结果:
6
此问题参考了 Howard Hinnant 对问题
在他的回答底部,他说:
Note that in this latest design, if your client ever does this:
X&& x = a + b + c;
then x is a dangling reference (which is why std::string does not do this).
cppreference.com 上的“引用初始化”一文的“临时对象的生命周期”段落列出了绑定到引用的临时对象的生命周期规则的例外情况。一个是:
"a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference."
我认为它的意思是“如果函数 returns 引用 引用参数绑定到 的临时对象”,而不仅仅是其他引用。因此,我认为这就是解释 Howard Hinnant 上述陈述的规则。
以下示例基于我所指的问题中给出的示例:
struct X
{
int _x;
X() : _x(0) {}
X(int x) : _x(x) {}
X(X const& other) : _x(other._x) {}
X(X&& other) noexcept : _x(other._x) { other._x = 0; std::cout << "Move from " << &other << " to " << this << std::endl; }
X& operator+=(const X& other) { _x += other._x; return *this; }
friend X operator+(X const& lhs, X const& rhs)
{
std::cout << "X const& lhs: " << &lhs << std::endl;
X temp = lhs;
temp += rhs;
return temp;
}
friend X&& operator+(X&& lhs, X const& rhs)
{
std::cout << "X&& lhs: " << &lhs << std::endl;
lhs += rhs;
return std::move(lhs);
}
};
int anotherFunc(int a)
{
int bigArray[3000]{};
std::cout << "ignore:" << &bigArray << std::endl;
int b = a * a;
std::cout << "int b: " << &b << std::endl;
return 2 * b;
}
int main()
{
X a(1), b(2), c(3), d(4);
X&& sum = a + b + c + d;
std::cout << "X&& sum: " << &sum << std::endl;
anotherFunc(15);
std::cout << "sum._x: " << sum._x << std::endl;
return 0;
}
这会打印
X const& lhs: 000000907DAFF8B4
Move from 000000907DAFF794 to 000000907DAFFA14
X&& lhs: 000000907DAFFA14
X&& lhs: 000000907DAFFA14
X&& sum: 000000907DAFFA14
ignore:000000907DAFC360
int b: 000000907DAFF254
sum._x: 10
使用 MSVC 编译时;以及使用 gcc 或 clang 编译时的类似输出。
sum
应该是这里的悬空引用。尽管如此,仍会打印出正确的值“10”。它甚至可以在 sum
的引用初始化和通过所述引用进行访问之间将大型数组推入堆栈时起作用。用于 sum 引用的临时对象的内存不会被重用,并且总是在别处分配(相对于以下函数调用的堆栈帧),无论下一个堆栈帧有多大或多小。
为什么我测试过的每个编译器都保留 X&& operator+(X&& lhs, X const& rhs)
本地的临时对象,即使 sum
根据 cppreference.com 上的规则应该是一个悬空引用。或者,更准确地说:尽管访问悬垂引用是未定义的行为,但为什么 每个 编译器都以这种方式实现它?
我喜欢为这种情况保留一个示例 class A
。 A
的完整定义有点太长,无法在此处列出,但完整包含 at this link.
简而言之,A
保留了一个 state
和一个 status
,而 status
可以是这些枚举之一:
destructed = -4,
self_move_assigned = -3,
move_assigned_from = -2,
move_constructed_from = -1,
constructed_specified = 0
即特殊会员相应设置状态。例如 ~A()
看起来像这样:
~A()
{
assert(is_valid());
--count;
state_ = randomize();
status_ = destructed;
}
并且有一个流媒体运营商打印这个class。
语言律师免责声明:打印出一个破坏的A
是未定义的行为,任何事情都可能发生。话虽这么说,当在优化 关闭 的情况下编译实验时,您通常会得到预期的结果。
对我来说,在 -O0
使用 clang
,这个:
#include "A.h"
#include <iostream>
int
main()
{
A a{1};
A b{2};
A c{3};
A&& x = a + b + c;
std::cout << x << '\n';
}
输出:
destructed: -1002199219
将行更改为:
A x = a + b + c;
结果:
6