在clang的C ++ 11状态页面中遇到了一个名为“ * this的右值引用”的提案。
我已经阅读了很多有关右值引用并理解它们的知识,但我认为我对此并不了解。使用这些条款,我在网络上也找不到太多资源。
该页面上有一份建议书链接:N2439(将移动语义扩展到* this),但我从那里也没有太多示例。
这个功能是关于什么的?
在clang的C ++ 11状态页面中遇到了一个名为“ * this的右值引用”的提案。
我已经阅读了很多有关右值引用并理解它们的知识,但我认为我对此并不了解。使用这些条款,我在网络上也找不到太多资源。
该页面上有一份建议书链接:N2439(将移动语义扩展到* this),但我从那里也没有太多示例。
这个功能是关于什么的?
Answers:
首先,“ * this的ref-qualifiers”仅仅是一个“营销声明”。*this
永不改变的类型,请参阅这篇文章的底部。但是,用这种措词更容易理解它。
接下来,以下代码根据函数†的“隐式对象参数” 的ref限定符选择要调用的函数:
// 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候选函数集可以包含要针对同一参数列表解析的成员和非成员函数。为了使参数列表和参数列表在此异构集中具有可比性,因此成员函数被视为具有一个额外的参数,称为隐式对象参数,该参数表示已为其调用成员函数的对象。[...]
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;
这就是为什么成员函数和非成员函数包含在所谓的重载集中的原因。为了降低分辨率,标准引号的粗体部分存在。另外,这对我们来说很重要(相同的条款):
p4对于非静态成员函数,隐式对象参数的类型为
不带ref限定符或带ref限定符声明的函数的“对cv的 左值引用
X
”&
使用ref限定符声明的函数的“对cv的 右值引用
X
”&&
其中
X
,函数是成员的类,而cv是成员函数声明中的cv-qualification。[...]p5在重载解析期间,隐式对象参数保留其标识,因为对相应参数的转换应遵循以下附加规则:
不能引入任何临时对象来保存隐式对象参数的参数;和
不能应用用户定义的转换来实现与它的类型匹配
[...]
(最后一点只是意味着您不能基于调用成员函数(或运算符)的对象的隐式转换来欺骗重载解决方案。)
让我们以本文开头的第一个示例为例。经过上述转换后,过载集如下所示:
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
它将匹配一个rvalue(still §13.3.1
):
p5 [...]对于声明为不带ref限定符的非静态成员函数,应遵循一条附加规则:
- 即使隐式对象参数不是
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
是prvalue表达式,其值是为其调用该函数的对象的地址。类的this
成员函数中的类型X
为X*
。[...]
MyType(int a, double b) &&
吗?
左值ref限定符形式还有一个用例。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限定符解决了以下问题:
struct S {
S& operator ++() &;
S* operator &() &;
const S& operator =(const S&) &;
};
现在,运算符的工作方式类似于内置类型,仅接受左值。
假设您在一个类上有两个函数,它们的名称和签名都相同。但是其中之一被声明为const
:
void SomeFunc() const;
void SomeFunc();
如果类实例不是const
,则重载解析将优先选择非const版本。如果实例为const
,则用户只能调用const
版本。而this
指针是一个const
指针,以便实例不能被改变。
“为此的r值引用”的作用是让您添加另一种替代方法:
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值限定符this
,或者都不使用。您不能这样做:
void SomeFunc();
void SomeFunc() &&;
您必须这样做:
void SomeFunc() &;
void SomeFunc() &&;
请注意,此声明更改的类型*this
。这意味着&&
版本将所有访问成员作为r值引用。因此,可以轻松地从对象内部移动。该提案的第一个版本中给出的示例是(请注意:以下对于最终版本的C ++ 11可能是不正确的;这直接来自最初的“来自此提案的r值”):
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
第二版,不是吗?另外,为什么右值引用返回?
*this
,但是我可以理解混乱的根源。这是因为ref-qualifier会在重载解析和函数调用期间更改“ this”(此处是故意使用的!)对象所绑定的隐式(或“ hidden”)函数参数的类型。因此,*this
由于Xeo对此进行了修复,因此没有更改。相反,“hiddden”参数的改变,使其lvalue-或右值引用,就像const
函数限定符使它const
等