返回this的右值引用的正确方法
Correct way to return an rvalue reference to this
下面的代码导致未定义行为。请务必阅读所有答案以确保完整。
当通过operator<<
链接一个对象时,我想保留对象的左值性/右值性:
class Avenger {
public:
Avenger& operator<<(int) & {
return *this;
}
Avenger&& operator<<(int) && {
return *this; // compiler error cannot bind lvalue to rvalue
return std::move(*this);
}
};
void doJustice(const Avenger &) {};
void doJustice(Avenger &&) {};
int main() {
Avenger a;
doJustice(a << 24); // parameter should be Avenger&
doJustice(Avenger{} << 24); // parameter should be Avenger&&
return 0;
}
我不能简单地返回*this
,这意味着rvalue
对象的*this
类型仍然是lvalue reference
。我本来希望成为一名rvalue reference
。
- 是否正确/建议在
&&
限定符重载的成员上返回std::move(*this)
,或者应该使用其他方法?我知道std::move
只是一个演员,所以我认为是可以的,我只是想再检查一下。 -
rvalue
的*this
是lvalue reference
而不是rvalue reference
的原因/解释是什么? - 我记得在c++ 14中看到一些关于
*this
移动语义的东西。这和这个有关系吗?以上这些在c++ 14中会改变吗?
this
的类型取决于成员函数的cv-限定符:Avenger*
或const Avenger*
但不在其修饰符上。引用限定符仅用于确定要调用的函数。
因此,无论是否使用&&
, *this
的类型都是Avenger&
或const Avenger&
。不同之处在于,当被调用的对象是r值时,将使用&&
的重载,而&
则不会。
注意,右值性是表达式的属性,而不是对象的属性。例如:
void foo(Avenger &x)
{
foo(x); //recursive call
}
void foo(Avenger &&x)
{
foo(x); //calls foo(Avenger &)!
}
也就是说,尽管在第二个foo()
中,x
被定义为一个r值引用,但表达式x
的任何使用仍然是一个l值。对于*this
也是如此。
所以,如果你想移动对象,return std::move(*this)
是正确的方式。
如果this
被定义为参考值而不是指针,情况会有所不同吗?我不确定,但我认为将*this
视为r值可能会导致一些疯狂的情况…
我没有听说在c++ 14中有任何改变,但我可能错了…
std::move
或许更适合称为rvalue_cast
。
但它不是那样叫的。尽管它的名字,它只是一个右值强制转换:std::move
不移动。
所有命名值都是左值,所有指针解引用都是左值,因此使用std::move
或std::forward
(又称条件右值强制转换)将在声明点(或其他原因)为右值引用的命名值转换为特定点的右值是合适的。
std::move
,但现在它实际上触发移动到返回值。现在,如果您在引用(例如auto&& foo = expression;
)中捕获返回值,则引用生命周期扩展可以正常工作。返回右值引用的唯一好时机是在右值强制转换中:这使得move
是一个右值强制转换的事实有点学术性。
这个回答是对bolov在回答他的问题下给我的评论的回应。
#include <iostream>
class Avenger
{
bool constructed_ = true;
public:
Avenger() = default;
~Avenger()
{
constructed_ = false;
}
Avenger(Avenger const&) = default;
Avenger& operator=(Avenger const&) = default;
Avenger(Avenger&&) = default;
Avenger& operator=(Avenger&&) = default;
Avenger& operator<<(int) &
{
return *this;
}
Avenger&& operator<<(int) &&
{
return std::move(*this);
}
bool alive() const {return constructed_;}
};
void
doJustice(const Avenger& a)
{
std::cout << "doJustice(const Avenger& a): " << a.alive() << 'n';
};
void
doJustice(Avenger&& a)
{
std::cout << "doJustice(Avenger&& a): " << a.alive() << 'n';
};
int main()
{
Avenger a;
doJustice(a << 24); // parameter should be Avenger&
doJustice(Avenger{} << 24); // <--- this one
// Avenger&& dangling = Avenger{} << 24;
// doJustice(std::move(dangling));
}
这将可移植地输出:
doJustice(const Avenger& a): 1
doJustice(Avenger&& a): 1
上面的输出表明,一个临时的Avenger
对象将不会被销毁,直到上面注释"//<——this one"前面的';'所划分的序列点。
我已经从这个程序中删除了所有未定义的行为。这是一个完全兼容和可移植的程序
NOT ok在为&&
限定符重载的成员上返回std::move(*this)
。这里的问题不在于std::move(*this)
(其他答案正确地表明它是可以的),而在于返回类型。这个问题非常微妙,c++11标准化委员会几乎已经解决了这个问题。Stephan T. Lavavej在他的演讲Don 't Help the Compiler during Going Native 2013
中解释了这一点。他的例子可以在链接视频的第42分钟左右找到。他的例子略有不同,没有涉及*this
,并且使用按参数引用类型而不是按方法引用限定符重载,但原理仍然相同。
那么代码有什么问题呢?
简要介绍:绑定到临时对象的引用会延长该临时对象的生命周期。这就是为什么这样的代码是正确的:
void foo(std::string const & s) {
//
}
foo("Temporary std::string object constructed from this char * C-string");
这里的重要部分是这个属性不是可传递的,这意味着要延长临时对象的生命周期,它必须将直接绑定到到临时对象,而不是对它的引用。
回到我的例子:
为了完整起见,让我们添加一个函数,它只接受对Avenger
的const左值引用(没有右值引用重载):
void doInjustice(Avenger const &) {};
如果在函数内部引用参数,则接下来的两次调用将导致UB:
doInjustice(Avenger{} << 24); // calls `void doInustice(Avenger const &) {};`
doJustice(Avenger{} << 24); // calls `void doJustice(Avenger &&) {};`
在参数求值时构造的临时对象会在函数被调用后立即销毁,原因如上所述,并且参数是悬空引用。在函数中引用它们将导致UB。
正确的方式是按值返回:
class Avenger {
public:
Avenger& operator<<(int) & {
return *this;
}
Avenger operator<<(int) && {
return std::move(*this);
}
};
使用move语义仍然可以避免复制,并且返回值是临时的,这意味着它将调用正确的重载,但是我们避免了这个微妙但令人讨厌的无声错误。
Stephan T. Lavavej
示例:Don 't Help The Compiler (42m-45m)
string&& join(string&& rv, const char * ptr) {
return move(rv.append(", ").append(ptr));
}
string meow() { return "meow"; }
const string& r = join(meow(), "purr");
// r refers to a destroyed temporary!
//Fix:
string join(string&& rv, const char * ptr) {
return move(rv.append(", ").append(ptr));
}
关于SO通过引用解释临时对象寿命延长的帖子:
- const引用是否会延长临时对象的生命周期?
- 初始化引用
我的c++标准(第290页,15.2节,第6点)的git副本说:
因此,隐式this '形参'传递给"引用绑定到
的生命周期规则的例外情况临时延长生存期的那个临时]都是:sub 9 -类中绑定到引用形参的临时对象函数调用一直持续到包含调用的全表达式。
sub 10 -返回值的临时绑定的生存期在函数中return语句(9.6.3)没有扩展;对象结束时销毁临时对象返回语句中的完整表达式。[…]"
Avenger&& operator<<(int) &&
{
return std::move(*this);
}
在;调用operator<<在临时对象上,如果绑定了引用,则甚至。所以这个失败了:
Avenger &&dangler = Avenger{} << 24; // destructor already called
dangler << 1; // Operator called on dangling reference
如果OTOH返回值:
Avenger operator<<(int) &&
{
return std::move(*this);
}
,那么这些痛苦就不会发生,而且通常也没有额外的复制成本。事实上,如果你不让编译器通过move-construct from *this来创建返回值,那么它就会复制一个。因此,从遗忘的边缘攫取*,并拥有两全其美。
- 在他自己的方法中,有可能将一个对象取消引用到另一个对象吗
- 在 cpp 文件中隐藏采用模板参数引用的方法
- 拥有映射的现代方法,该映射可以指向或引用已在堆栈上分配的不同类型的数据
- 为什么常量方法可以采用非常量引用?
- 没有取消引用/解包对象的标准方法?
- C++方法中的引用变量
- 引用文件的适当方法是什么?
- C++有什么方法可以在既不调用函数模板也不提供其模板参数的情况下引用函数模板?
- 对结构方法的未定义引用
- 通过基类接受方法转发派生 UniquePtr 的右值会移动引用而不是复制
- 使用移动语义:右值引用作为方法参数
- Unity3D 导入 C++ DLL 用于按引用方法传递
- 通过引用方法传递对象
- 无法将从函数返回的向量传递给需要引用方法的函数
- 正在调用指针和引用方法
- 与const引用(方法链)相关联的临时对象的生命周期
- 方法内部的简单c++成员引用(方法解析后的链接器错误)
- 被引用方法的未定义引用
- 无法引用方法
- 我如何在外面引用 C++ 方法