什么是"rvalue reference for *this"?

What is "rvalue reference for *this"?

本文关键字:this for reference rvalue 什么      更新时间:2023-10-16

在clang的c++ 11状态页中遇到了一个名为" *this的右值引用"的建议。

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

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

这个特性是关于什么的?

首先,*this"只是一个"营销宣言"。*this的类型永远不会改变,请参阅本文底部。这样的措辞更容易理解。

接下来,下面的代码根据"隐式对象参数"的ref-限定符选择要调用的函数。函数:
// t.cpp
#include <iostream>
struct test{
void f() &{ std::cout << "lvalue objectn"; }
void f() &&{ std::cout << "rvalue objectn"; }
};
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]

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

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

为什么还要比较成员函数和非成员函数呢?运算符重载,这就是原因。想想看:

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对于非静态成员函数,隐式对象形参的类型为

  • "对cvX的左值引用"对于未使用ref-qualifier或使用&ref-qualifier

    声明的函数
  • 对于用&&ref-qualifier

    声明的函数"对cvX的右值引用">

,其中X是该函数所属的类,cv是成员函数声明上的cv限定符。[…]

p5在重载解析过程中[…][t]隐式对象参数[…]]保持其同一性,因为对相应实参的转换必须遵守以下附加规则:

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

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

[…]

(最后一位只是表示不能通过调用成员函数(或操作符)的对象的隐式转换来欺骗重载解析)

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

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-限定符声明的非静态成员函数,应用一个额外的规则:

  • 即使隐式对象形参不符合const,只要在所有其他方面实参可以转换为隐式对象形参的类型,则可以将右值绑定到形参上。
struct test{
void f() { std::cout << "lvalue or rvalue objectn"; }
};
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是一个右值表达式,其值是调用该函数的对象的地址。类X的成员函数this的类型为X*。[…]

左值引用限定符表单还有一个额外的用例。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&) &;
};

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

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

void SomeFunc() const;
void SomeFunc();

如果类实例不是const,重载解析将优先选择非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值引用版本和非引用版本之间重载。也就是说,如果您有一个成员函数名,那么它的所有版本要么在this上使用l/r值限定符,要么都不使用。你不能这样做:

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
相关文章: