256

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

我已经阅读了很多关于右值引用并理解它们的内容,但我认为我不知道这一点。使用这些术语,我也无法在网上找到太多资源。

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

这个功能是关于什么的?

4

3 回答 3

309

首先,“*this 的引用限定符”只是一个“营销声明”。永不改变的类型*this,请参阅这篇文章的底部。不过,用这种措辞更容易理解它。

接下来,以下代码根据函数†</sup>的“隐式对象参数”的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 限定符( constand volatile) 和ref-qualifiers ( &and &&)。


注意:这里后面有很多标准引号和重载解析说明!

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

将调用哪个函数由称为重载决策的过程确定。这个过程相当复杂,所以我们只会触及对我们重要的部分。

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

§13.3.1 [over.match.funcs]

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

p3 类似地,在适当的时候,上下文可以构造一个参数列表,其中包含一个隐含的对象参数来表示要操作的对象。

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

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

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

您当然希望以下调用免费功能,不是吗?

char const* s = "free foo!\n";
foo f;
f << s;

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

p4 对于非静态成员函数,隐式对象参数的类型为

  • “对cv 的左值引用X”用于声明没有ref-qualifier& ref -qualifier 的函数

  • 使用ref 限定符声明的函数的“对cv 的右值引用”X&&

其中X是函数是其成员的类,cv是成员函数声明上的 cv 限定。[...]

p5 在重载解析期间 [...] [t] 隐式对象参数 [...] 保留其身份,因为相应参数的转换应遵守以下附加规则:

  • 不能引入临时对象来保存隐式对象参数的参数;和

  • 不能应用任何用户定义的转换来实现与它的类型匹配

[...]

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

让我们以本文顶部的第一个示例为例。在上述转换之后,重载集看起来像这样:

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 限定符(因此没有重载函数),f1 它将匹配一个右值(仍然§13.3.1):

p5 [...] 对于没有ref-qualifier声明的非静态成员函数,适用附加规则:

  • 即使隐式对象参数不是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.

那是错误的,*this始终左值:

§5.3.1 [expr.unary.op] p1

一元运算*符执行间接:应用它的表达式应该是指向对象类型的指针,或指向函数类型的指针,结果是一个左值,指向表达式指向的对象或函数。

§9.3.2 [class.this] p1

在非静态 (9.3) 成员函数的主体中,关键字this是纯右值表达式,其值是调用该函数的对象的地址。类的this成员函数中的类型XX*。[...]

于 2011-12-22T23:09:54.930 回答
80

左值引用限定符形式还有一个用例。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...

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

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

现在操作符像内置类型一样工作,只接受左值。

于 2011-12-23T09:14:10.377 回答
29

假设您在一个类上有两个函数,它们都具有相同的名称和签名。但是其中之一是声明的const

void SomeFunc() const;
void SomeFunc();

如果类实例不是const,重载决议将优先选择非常量版本。如果实例是const,用户只能调用const版本。而且this指针是const指针,所以不能改变实例。

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

void RValueFunc() &&;

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

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

这样,您可以根据是否通过 r 值访问对象来专门化行为。

请注意,不允许在 r 值参考版本和非参考版本之间重载。也就是说,如果你有一个成员函数名,它的所有版本要么使用 l/r 值限定符 on this,要么都不使用。你不能这样做:

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
于 2011-12-22T23:05:16.777 回答