我已经阅读了很多关于rvalue引用的内容,并且理解了它们,但我不认为我了解这个。我在网上也找不到很多使用这些术语的资源。
页面上有一个指向提案论文的链接:N2439(将移动语义扩展到*this),但我在那里也没有找到很多示例。
这个特性是关于什么的?
首先,“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);
}
};
const
和volatile
)和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
char const* s = "free foo!\n";
foo f;
f << s;
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
struct test{
void f() { std::cout << "lvalue or rvalue object\n"; }
};
int main(){
test t;
t.f(); // OK
test().f(); // OK too
}
对于左值引用限定符形式还有一个额外的用例。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。
假设在一个类中有两个函数,这两个函数的名称和签名是相同的。但其中一个被声明为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
std::move
来移动第二个版本,不是吗?另外,为什么要使用右值引用返回? - Xeo*this
的类型,但我可以理解混淆的原因。这是因为引用限定符(ref-qualifier)更改了隐式(或“隐藏”的)函数参数的类型,在重载决议和函数调用期间,“this”(引号故意放在这里!)对象被绑定到该函数参数上。因此,“*this”没有改变,正如Xeo所解释的那样。而是改变了“隐藏”的参数,使其成为左值引用或右值引用,就像const函数限定符使其成为const等一样。 - bronekk
*this
的类型能根据有效的 ref 限定符变为T&
或T&&
,那就很酷了。你可能会担心这意味着this
将成为指向引用的指针,这是不可能的。但我认为我们可以通过说,在具有 ref 限定符的方法中,this
不再是原始指针来避免这个问题。相反,它将是一个类似指针的类型,其重载和转换已定义。也许现在引入这个变化有点晚了,或许应该在 ref 限定符方法一起引入。 - Aaron McDaid