在clang的c++ 11状态页面中遇到了一个叫做“*this的右值引用”的提议。

我读过很多关于右值引用的书,也理解它们,但我不认为我知道这个。我在网上也找不到多少使用这些术语的资源。

页面上有一个提案论文的链接:N2439(将移动语义扩展到*this),但我也没有从那里得到太多示例。

这个特性是关于什么的?


假设一个类上有两个函数,它们的名称和签名都相同。但其中一个被声明为const:

void SomeFunc() const;
void SomeFunc();

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

“r-value reference for this”所做的是允许你添加另一个选项:

void RValueFunc() &&;

这允许您拥有一个只有在用户通过适当的r-value调用它时才能调用的函数。如果这是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-值引用版本和非引用版本之间重载。也就是说,如果你有一个成员函数名,它的所有版本要么在它上面使用l/r-value限定符,要么都不使用。你不能这样做:

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

你必须这样做:

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

注意,这个声明改变了*this的类型。这意味着&&将所有访问成员版本为r值引用。这样就可以很容易地从物体内部移动。提案的第一个版本中给出的示例是(注意:以下内容对于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

首先,“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

这样做的目的是为了利用调用函数的对象是右值(例如,未命名的临时对象)这一事实。下面的代码作为进一步的例子:

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-限定符(const和volatile)和ref-限定符(&和&&)。


注:很多标准报价和重载解析说明在这里之后!

†为了理解这是如何工作的,以及为什么@Nicol Bolas的答案至少有一部分是错误的,我们必须深入挖掘c++标准(解释为什么@Nicol的答案是错误的部分在底部,如果你只对这个感兴趣的话)。

调用哪个函数是由一个称为重载解析的进程决定的。这个过程相当复杂,所以我们只涉及对我们来说重要的部分。

首先,重要的是要了解成员函数的重载解析是如何工作的:

§13.3.1 [over.match.funcs]

p2 The set of candidate functions can contain both member and non-member functions to be resolved against the same argument list. So that argument and parameter lists are comparable within this heterogeneous set, a member function is considered to have an extra parameter, called the implicit object parameter, which represents the object for which the member function has been called. [...] p3 Similarly, when appropriate, the context can construct an argument list that contains an implied object argument to denote the object to be operated on.

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

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;

这就是为什么成员函数和非成员函数都包含在所谓的重载集中。为了使解析不那么复杂,标准引用的粗体部分存在。此外,这对我们来说很重要(同一从句):

p4 For non-static member functions, the type of the implicit object parameter is “lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier “rvalue reference to cv X” for functions declared with the && ref-qualifier where X is the class of which the function is a member and cv is the cv-qualification on the member function declaration. [...] p5 During overload resolution [...] [t]he implicit object parameter [...] retains its identity since conversions on the corresponding argument shall obey these additional rules: no temporary object can be introduced to hold the argument for the implicit object parameter; and no user-defined conversions can be applied to achieve a type match with it [...]

(最后一点只是意味着您不能基于调用成员函数(或操作符)的对象的隐式转换来欺骗重载解析。)

让我们看看这篇文章开头的第一个例子。在前面提到的转换之后,重载集看起来像这样:

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将匹配右值(仍然是§13.3.1):

p5[…对于声明时没有引用限定符的非静态成员函数,应用一个附加规则: 即使隐式对象形参不是const限定的,只要在所有其他方面实参都可以转换为隐式对象形参的类型,就可以将右值绑定到形参。

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

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

现在,来看看为什么@Nicol的答案至少有一部分是错的。他说:

注意,这个声明改变了*this的类型。

这是错误的,*总是左值:

§5.3.1 [expr.unary。op) p1

一元*操作符执行间接操作:它所应用的表达式必须是指向对象类型的指针,或者指向函数类型的指针,结果是指向表达式所指向的对象或函数的左值。

§9.3.2[类。这p1

在非静态(9.3)成员函数体中,关键字this是一个prvalue表达式,其值是调用该函数的对象的地址。在类X的成员函数中this的类型是X*。[…]


左值ref-qualifier表单还有一个额外的用例。c++ 98的语言允许为右值类实例调用非const成员函数。这就导致了各种各样的奇怪,违背了右值的概念,并偏离了内置类型的工作方式:

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...

左值ref-qualifier可以解决以下问题:

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

现在,操作符的工作方式与内置类型类似,只接受左值。