返回this的右值引用的正确方法

Correct way to return an rvalue reference to this

本文关键字:引用 方法 this 返回      更新时间:2023-10-16

下面的代码导致未定义行为。请务必阅读所有答案以确保完整。


当通过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*thislvalue 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::movestd::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副本说:

"引用绑定到
的生命周期规则的例外情况临时延长生存期的那个临时]都是:

sub 9 -类中绑定到引用形参的临时对象函数调用一直持续到包含调用的全表达式。

sub 10 -返回值的临时绑定的生存期在函数中return语句(9.6.3)没有扩展;对象结束时销毁临时对象返回语句中的完整表达式。[…]"

因此,隐式this '形参'传递给
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来创建返回值,那么它就会复制一个。因此,从遗忘的边缘攫取*,并拥有两全其美。