正确的方法返回一个右值引用

时间:2014-05-26 16:36:06

标签: c++ c++11 c++14 rvalue

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


通过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,这意味着*this对象的rvalue类型仍然是lvalue reference。我本来期望成为rvalue reference

  • 是否正确/建议在std::move(*this)限定符重载的成员上返回&&,还是应该使用其他方法?我知道std::move只是演员,所以我觉得没关系,我只是想仔细检查一下。
  • *this的{​​{1}}是rvalue而不是lvalue reference的原因/解释是什么?
  • 我记得在C ++ 14中看到rvalue reference的移动语义。这与此有关吗?上述任何一项都会在C ++ 14中发生变化吗?

5 个答案:

答案 0 :(得分:11)

this的类型取决于成员函数的cv限定符:Avenger*const Avenger*

但不是在 ref-qualifier 上。 ref-qualifier 仅用于确定要调用的函数。

因此,*this的类型为Avenger&const Avenger&,无论您是否使用&&。区别在于将使用&&的重载,然后被调用的对象是r值,而&则不会。

请注意,rvalue-ness是表达式的属性,而不是对象。例如:

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中没有听说过有关此事的任何变化,但我可能会弄错......

答案 1 :(得分:7)

std::move可能更好地称为rvalue_cast

但它没有被称为。尽管它的名字,它只不过是一个右手投射:std::move不会移动。

所有命名值都是左值,所有指针取消引用都是如此,因此使用std::movestd::forward(也称为条件右值转换)来转换在声明点处作为右值引用的命名值(或其他原因)在特定点进入右值是犹太洁食。

但请注意,您很少想要返回右值引用。如果你的类型移动便宜,你通常想要返回一个文字。这样做在方法体中使用相同的std::move,但现在它实际上触发了移动到返回值。现在,如果您在引用中捕获返回值(例如auto&& foo = expression;),则引用生存期扩展正常工作。关于返回右值引用的唯一好时机是rvalue强制转换:哪种情况使move是一个rvalue转换有点学术性。

答案 2 :(得分:4)

这个答案是对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

以上输出显示的是,在由&#39;;&#39;划分的序列点之前,不会破坏临时Avenger对象。就在评论之前&#34; //&lt; ---这一个&#34;上方。

我已从此程序中删除了所有未定义的行为。这是一个完全符合要求的便携式程序。

答案 3 :(得分:1)

对于std::move(*this)限定符重载的成员,返回&& 不正常。这里的问题不在于std::move(*this)(其他答案正确地表明它没问题),而是返回类型。问题非常微妙,几乎是由c ++ 11标准化委员会完成的。 Stephan T. Lavavej Going Native 2013期间的演讲Don’t Help the Compiler中对此进行了解释。他的例子可以在链接视频的大约42分钟找到。他的例子略有不同,不涉及*this并且使用参数引用类型的重载而不是方法引用限定符,但原则仍然相同。

那么代码有什么问题?

简短介绍:绑定到临时对象的引用会延长临时对象在引用生命周期内的生命周期。这就是使这样的代码没问题的原因:

void foo(std::string const & s) {
  //
}

foo("Temporary std::string object constructed from this char * C-string");

这里的重要部分是这个属性不是传递的,这意味着对于延长临时对象生命周期的引用,它必须将直接绑定到临时对象,而不是引用它。

回到我的例子:

对于completness,让我们添加一个只对Avenger进行常量左值引用的函数(没有右值引用过载):

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);
    }
};

移动语义仍然会复制一个副本,并且返回是临时的,这意味着它将调用正确的重载,但我们避免使用这个微妙但令人讨厌的无声错误。


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的帖子通过引用解释临时对象的寿命延长:

答案 4 :(得分:1)

我的C ++标准的git副本(第290页,第15.2节,第6点)说:

  

&#34;此生命周期的例外规则[引用绑定到
  暂时延长那个临时的生命周期是:

     

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

     

sub 10 - 临时绑定到返回值的生命周期              在函数返回语句(9.6.3)中没有扩展;              临时在结束时被摧毁              return语句中的完整表达式。 [...]&#34;

因此,暗示这个'参数&#39;传递给

Avenger&& operator<<(int) &&
{
      return std::move(*this);
}

结束生命;调用operator&lt;&lt;的语句在临时对象上,甚至如果引用绑定到它。所以这失败了:

Avenger &&dangler = Avenger{} << 24; // destructor already called
dangler << 1; // Operator called on dangling reference

如果OTOH的回报是按值计算的:

Avenger operator<<(int) &&
{
      return std::move(*this);
}
然后,没有任何痛苦发生,并且在复制方面通常没有额外的成本。事实上,如果你不提供编译器通过从* this移动构造来创建返回值,那么它就会复制。 所以从遗忘的边缘抓住这个,并且两全其美。