什么是“this的右值参考”?

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

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

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

这个特性是关于什么的?

33032 次浏览

假设一个类上有两个函数,它们的名称和签名都相同。但是其中一个被声明为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-值引用版本和非引用版本之间重载。也就是说,如果你有一个成员函数名,它的所有版本要么在this上使用l/r-value限定符,要么都不使用。你不能这样做:

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

首先,“this”的修饰词;只是一份“营销声明”。*this的类型永远不会改变,请参阅本文底部。不过,用这种措辞更容易理解。

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

// 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-qualifiers (constvolatile)和ref-qualifiers (&&&)。


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

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

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

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

§13.3.1 [over.match.funcs]

候选函数集可以包含针对同一个参数列表进行解析的成员函数和非成员函数。因此,实参和形参列表在这个异构集成员函数被认为有一个额外的形参,称为隐式对象形参,它表示成员函数被调用的对象中是可比较的。[…]

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

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

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;

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

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

  • " lvalue reference to 简历 X "用于声明没有ref-qualifier或带有& ref-qualifier的函数

  • "对简历 X的右值引用"用于使用&& ref-qualifier声明的函数

其中X是该函数所属的类,简历是成员函数声明中的cv-qualification。[…]

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-qualifier(因此没有重载函数),f1 匹配右值(仍然是§13.3.1):

p5[…对于没有ref-qualifier声明的非静态成员函数,应用一个附加规则:

  • 即使隐式对象形参不是__abc0限定的,只要在所有其他方面实参可以转换为隐式对象形参的类型,右值也可以绑定到形参。
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*。[…]

左值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&) &;
};

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