“rvalue references for *this”是用来做什么的?

24

“rvalue references for *this”(标准中也称成员函数的引用限定符)最典型的使用案例是什么?

顺便提一下,有一个非常好的关于这个语言特性的解释在这里


随着现在支持它的编译器,我预计很快会看到一两篇关于这个主题的文章出现。 - chris
gcc在4.8.1版本中刚实现了这个功能,所以还没有多少人使用过这个特性。 - Jesse Good
1
在你链接的问题中,示例和说明不足吗? - Ali
1
我经常看到的一个常见例子是只有左值的 operator=。例如 T& operator=(T const&); 将防止 T() = T(); 编译通过。 - R. Martinho Fernandes
@user1131467 哦,该死,是的。那非常愚蠢。 - R. Martinho Fernandes
显示剩余2条评论
3个回答

21

每个成员函数在调用时,都有一个隐式对象参数,*this 引用它。

因此,这些普通函数重载:

void f(const T&);
void f(T&&);

当像 f(x) 这样调用时;以及(b)这些成员函数重载:

struct C
{
    void f() const &;
    void f() &&;
};

当像x.f()这样调用时,(a)和(b)的可行性和排名非常相似。

因此,使用案例基本相同。 它们旨在支持移动语义优化。 在右值成员函数中,您可以基本上掠夺对象的资源,因为您知道它是一个即将过期的对象(即将被删除):

int main()
{
    C c;
    c.f(); // lvalue, so calls lvalue-reference member f
    C().f(); // temporary is prvalue, so called rvalue-reference member f
    move(c).f(); // move changes c to xvalue, so again calls rvalue-reference member f
}

例如:

struct C
{
    C operator+(const C& that) const &
    {
        C c(*this); // take a copy of this
        c += that;
        return c;
    }

    C operator+(const C& that) &&
    {
        (*this) += that;
        return move(*this); // moving this is ok here
    }
}

还要考虑那些在rvalue上调用没有意义的成员,比如赋值运算符。 - Puppy
@DeadMG: 是的,一个 rvalue (prvalue 或 xvalue)将无法有效地绑定到 void f(T&),因此在这里的用例与 T::f() & 相同,涉及隐式对象参数。我的主要观点是注意普通函数参数和隐式对象参数之间的正交关系 - 因此,对于普通函数参数适用的值类别和重载选择的一般理解可以直接重用为成员函数隐式对象参数(仅有不值得一提的小差异)。 - Andrew Tomazos
就我个人而言,我甚至不会费心去移动它。只需将其添加到自身并在之后移动即可。 - R. Martinho Fernandes
@R.MartinhoFernandes:这取决于移动构造函数和operator +=的实际操作,但在某些情况下可能更有效率。已更改,谢谢。 - Andrew Tomazos
@RalphTandetzky:这样做会冒险将用户绑定*this到一个不会扩展其生命周期的引用上。例如const C&c = C() + C(); c.foo();,在调用foo之前,c已被销毁。 - Andrew Tomazos
显示剩余2条评论

5

当针对rvalues进行调用时,一些操作会更加高效。因此,根据*this的值类别进行重载,可以自动使用最有效的实现方法。

struct Buffer
{
  std::string m_data;
public:
  std::string str() const& { return m_data; }        // copies data
  std::string str()&& { return std::move(m_data); }  // moves data
};

(据我所知,尽管可以对std :: ostringstream进行此优化,但尚未正式提出。)
一些操作在rvalue上调用没有意义,因此对*this进行重载允许删除rvalue形式:
struct Foo
{
  void mutate()&;
  void mutate()&& = delete;
};

我实际上还没有需要使用这个功能,但是现在我可能会发现更多的用途,因为我关心的两个编译器都支持它。


对于没有ref-qualifier声明的非静态成员函数,还有一个额外的规则适用:即使隐式对象参数没有const限定符,只要在所有其他方面参数可以转换为隐式对象参数的类型,rvalue就可以绑定到该参数上。因此,没有ref qualifier的Foo::mutate()可以针对rvalue进行操作... - Andrew Tomazos
关于排名的进一步说明:“S1和S2是参考绑定(8.5.3),它们都不引用非静态成员函数__声明而没有ref-qualifier的隐式对象参数,且S1将rvalue引用绑定到rvalue,S2将lvalue引用绑定。”因此,使用rvalue将在mutate() no-ref-qualifiermutate()&&之间产生歧义。这是您的意图吗? - Andrew Tomazos
我认为你最终得到了想要的效果,但诊断可能不太理想(错误模糊重载)。 - Andrew Tomazos
不,这不是我的意图,我只是在其中一个重载上错过了一个&,已经修复了。 - Jonathan Wakely
好的,那么我认为 void mutate()&& = delete; 是不必要的,因为一个 rvalue 不会绑定到 void mutate()& - Andrew Tomazos
我是否记错了,还是曾经有一段时间C++编译器(至少有些?)拒绝调用临时对象的非const方法?因此,这个设备是一种让安全性回归的方式,同时仍然允许在有意义的情况下进行这样的调用(例如,当临时对象是持久对象数据的“视图”,并且该方法不是const,因为它会更改所查看的数据)。 - greggo

1
在我的编译器框架中(即将发布),您可以将诸如令牌之类的信息传递到编译器对象中,然后调用finalize表示流的结束。不调用finalize而销毁对象是不好的,因为它不能清除所有输出。但是,析构函数不能执行finalize,因为它可能会抛出异常,如果解析器已经中止,则请求finalize获取更多输出也是错误的。当所有输入已经由另一个对象封装时,在rvalue编译器对象中传递输入很好。
pile< lexer, parser >( output_handler ).pass( open_file( "source.q" ) );

没有特殊的支持,这一定是不正确的,因为finalize没有被调用。接口不应该让用户做这样的事情。
首先要做的是排除finalize从未被调用的情况。如果使用左值引用限定符来调整原型,以上示例将被禁止:
void pass( input_file f ) & {
    process_the_file();
}

这样可以为添加另一个重载函数腾出空间,以便正确地完成对象。它是右值引用限定的,因此只有在调用即将过期的对象时才会选择它。
void pass( input_file f ) && {
    pass( std::move( f ) ); // dispatch to lvalue case
    finalize();
}

现在,用户几乎不需要担心记得调用finalize,因为大多数编译器对象最终都被实例化为临时对象。
注意,这种情况并不特定于ref-qualified成员。任何函数都可以有单独的t &t &&重载。目前实际实现中,pass使用完美转发,然后回溯以确定正确的语义。
template< typename compiler, typename arg >
void pass( compiler && c, arg a ) {
    c.take_input( a );

    if ( ! std::is_reference< compiler >::value ) {
        c.finalize();
    }
}

有很多方法可以处理重载。实际上,在未经修饰的成员函数中,不关心调用对象的类别(左值或右值),也不将该信息传递到函数中,这是不寻常的。除了隐式的this之外,任何函数参数都必须说明其参数的类别。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接