“rvalue reference for *this” 提案是什么意思?

268
在clang的C++11状态页面上看到了一个名为“rvalue reference for *this”的提案。
我已经阅读了很多关于rvalue引用的内容,并且理解了它们,但我不认为我了解这个。我在网上也找不到很多使用这些术语的资源。
页面上有一个指向提案论文的链接:N2439(将移动语义扩展到*this),但我在那里也没有找到很多示例。
这个特性是关于什么的?
3个回答

321

首先,“ref-qualifiers for *this”只是一个“营销用语”。请参见本帖底部,*this的类型从未更改。但使用这种措辞更容易理解。

接下来,以下代码根据函数的“隐式对象参数”的ref-qualifier选择要调用的函数

// t.cpp
#include <iostream>

struct test{
  void f() &{ std::cout << "lvalue object\n"; }
  void f() &&{ std::cout << "rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // lvalue
  test().f(); // rvalue
}

输出:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

整个过程是为了让您利用该函数在调用对象为rvalue(例如未命名临时对象)时的优势。以下代码可作为进一步示例:
struct test2{
  std::unique_ptr<int[]> heavy_resource;

  test2()
    : heavy_resource(new int[500]) {}

  operator std::unique_ptr<int[]>() const&{
    // lvalue object, deep copy
    std::unique_ptr<int[]> p(new int[500]);
    for(int i=0; i < 500; ++i)
      p[i] = heavy_resource[i];

    return p;
  }

  operator std::unique_ptr<int[]>() &&{
    // rvalue object
    // we are garbage anyways, just move resource
    return std::move(heavy_resource);
  }
};

这可能有点牵强,但你应该能明白。
请注意,您可以结合cv-qualifiers(constvolatile)和ref-qualifiers(&&&)。

注意:此处之后有许多标准引用和重载决议说明!

† 要理解这是如何工作的,以及为什么@Nicol Bolas的答案至少部分是错误的,我们必须深入研究一下C++标准(如果您只对此感兴趣,则@Nicol的答案错误的部分在底部)。

哪个函数将被调用是由一个称为overload resolution的过程确定的。这个过程相当复杂,所以我们只会涉及到对我们重要的部分。

首先,重要的是看看成员函数的重载决议是如何工作的:

§13.3.1 [over.match.funcs]

p2 候选函数集可以包含要针对相同参数列表解析的成员函数和非成员函数。因此,在这个异构集合中可以比较参数和参数列表,认为成员函数有一个额外的参数,称为隐式对象参数,它代表调用成员函数的对象。[...]

p3 同样,在适当的情况下,上下文可以构造一个包含隐式对象参数的参数列表,以表示要操作的对象。

为什么我们甚至需要比较成员函数和非成员函数?这就是运算符重载的原因。考虑这个:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

您肯定希望使用以下代码调用free函数,不是吗?
char const* s = "free foo!\n";
foo f;
f << s;

这就是为什么会将成员函数和非成员函数包含在所谓的重载集合中。为了使解析过程更简单,在标准引文的粗体部分存在。此外,这对我们来说非常重要(相同的条款):
对于非静态成员函数,隐式对象参数的类型为:
- 对于未声明引用限定符或使用&引用限定符的函数,“cv X的左值引用”,其中X是函数成员的类,cv是成员函数声明的cv限定符。 - 对于使用&&引用限定符声明的函数,“cv X的右值引用”。
其中X是函数成员的类,cv是成员函数声明的cv限定符。[...]
在重载决议期间, [...] 隐式对象参数保留其身份,因为相应参数上的转换必须遵守以下附加规则:
- 不能引入临时对象来保存隐式对象参数的参数; - 不能应用用户定义的转换来实现与其匹配的类型。
[...] (最后一点意味着不能根据调用成员函数(或运算符)的对象的隐式转换欺骗重载决议。)
让我们以本文顶部的第一个示例为例。经过前述转换后,重载集合看起来像这样:
void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

然后将包含一个隐式对象参数的参数列表与重载集合中包含的每个函数的参数列表进行匹配。在我们的情况下,参数列表只包含该对象参数。让我们看看这是什么样子:

// first call to 'f' in 'main'
test t;
f1(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

如果在测试了集合中的所有重载之后,只剩下一个重载,则重载解析成功,并调用与该转换重载链接的函数。对于第二次调用'f'也是如此:

// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
            // taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
            // kept in overload-set

请注意,如果我们没有提供任何ref-qualifier(因此未重载函数),那么f1将匹配一个rvalue(仍然是§13.3.1): p5[...]对于未声明ref-qualifier的非静态成员函数,还应用了一条附加规则:即使隐式对象参数未被const限定,只要在所有其他方面参数可以转换为隐式对象参数类型,则rvalue可以绑定到参数上。
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

现在我们来说明@Nicol的回答至少部分错误的原因。他说:
请注意,这个声明改变了*this的类型。
那是错的,*this永远都是一个左值:
§5.3.1 [expr.unary.op] p1
一元运算符*执行间接寻址:应用它的表达式必须是一个指向对象类型或函数类型的指针,并且结果是一个左值,该左值引用表达式所指向的对象或函数。
§9.3.2 [class.this] p1
在非静态(9.3)成员函数的函数体中,关键字this是一个prvalue表达式,其值是调用该函数的对象的地址。类X的成员函数中this的类型是X *。[......]

我认为“转换后”部分右侧的参数类型应该是'foo'而不是'test'。 - ryaner
@ryaner:发现得不错,谢谢。虽然不是参数,但函数的类标识符是错误的。 :) - Xeo
@GermánDiago,我希望能够将构造函数标记为“const”,以便为正在构建为“const”的类型进行构建。如果我们知道这个特定的对象永远不需要被修改,那么它可能允许我们以更有效的方式存储一些数据。但是,我不确定是否可以看到“&&”和“&”构造函数的用途。 - Aaron McDaid
如果 *this 的类型能根据有效的 ref 限定符变为 T&T&&,那就很酷了。你可能会担心这意味着 this 将成为指向引用的指针,这是不可能的。但我认为我们可以通过说,在具有 ref 限定符的方法中,this 不再是原始指针来避免这个问题。相反,它将是一个类似指针的类型,其重载和转换已定义。也许现在引入这个变化有点晚了,或许应该在 ref 限定符方法一起引入。 - Aaron McDaid
2
"*this的类型永远不会改变",你可能需要更清楚地表明它不会根据r/l-value资格而改变,但它可以在const/non-const之间改变。 - xaxxon
显示剩余4条评论

83

对于左值引用限定符形式还有一个额外的用例。C++98有一种语言,允许为rvalue调用非const成员函数的类实例。这导致了各种反常现象,违背了rvalueness的概念,并偏离了内置类型的工作方式:

struct S {
  S& operator ++(); 
  S* operator &(); 
};
S() = S();      // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S();           // taking address of rvalue...

左值引用限定符解决了这些问题:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

现在运算符的工作方式类似于内置类型,仅接受lvalues。


30

假设在一个类中有两个函数,这两个函数的名称和签名是相同的。但其中一个被声明为const

void SomeFunc() const;
void SomeFunc();

如果一个类实例不是const,则重载解析器会优先选择非const版本。如果实例是const,则用户只能调用const版本。而this指针是一个const指针,所以实例不能被改变。

"r-value reference for this" 的作用是允许您添加另一种选择:

void RValueFunc() &&;

这允许您拥有一个函数,只能在用户通过适当的右值调用它时才能调用。因此,如果它在类型Object中:

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

这种方式可以根据对象是通过r-value访问还是其他访问方式,来专门定制行为。

请注意,您不能在r-value参考版本和非引用版本之间进行重载。也就是说,如果您有一个成员函数名称,则其所有版本都要么使用l/r-value限定符this,要么没有使用。不可以这样做:

void SomeFunc();
void SomeFunc() &&;

你必须这样做:

void SomeFunc() &;
void SomeFunc() &&;

请注意,此声明更改了*this的类型。这意味着&&版本都将访问成员作为r-value引用。因此,可以轻松地从对象内部移动。建议第一个版本中提供的示例是(注意:以下内容可能无法与C++11的最终版本正确匹配;它直接来自于最初的“r-value from this”提案):

class X {
   std::vector<char> data_;
public:
   // ...
   std::vector<char> const & data() const & { return data_; }
   std::vector<char> && data() && { return data_; }
};

X f();

// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move

2
我认为你需要使用std::move来移动第二个版本,不是吗?另外,为什么要使用右值引用返回? - Xeo
1
@Xeo:因为这是提案中的示例;我不知道它是否仍适用于当前版本。而返回 r-value 引用的原因是因为移动应该由捕获它的人决定。它不应该立即发生,以防他实际上想将其存储在 && 中而不是值中。 - Nicol Bolas
3
对的,我有点想到了我第二个问题的原因。不过我在想,一个临时对象的成员的右值引用是否会延长该临时对象或其成员的生命周期呢?我记得有一段时间在SO上看到过这样的问题…… - Xeo
1
@Xeo:这不完全正确。 如果存在非const版本,则重载决议将始终选择非const版本。 您需要进行类型转换才能获得const版本。 我已更新帖子以澄清此点。 - Nicol Bolas
19
我创造了C++11中的这个功能,所以我想解释一下。Xeo正确地坚持认为这不会改变*this的类型,但我可以理解混淆的原因。这是因为引用限定符(ref-qualifier)更改了隐式(或“隐藏”的)函数参数的类型,在重载决议和函数调用期间,“this”(引号故意放在这里!)对象被绑定到该函数参数上。因此,“*this”没有改变,正如Xeo所解释的那样。而是改变了“隐藏”的参数,使其成为左值引用或右值引用,就像const函数限定符使其成为const等一样。 - bronekk
显示剩余6条评论

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