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


238

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

我已经阅读了很多有关右值引用并理解它们的知识,但我认为我对此并不了解。使用这些条款,我在网络上也找不到太多资源。

该页面上有一份建议书链接:N2439(将移动语义扩展到* this),但我从那里也没有太多示例。

这个功能是关于什么的?

Answers:


293

首先,“ * 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限定词constvolatile)和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成员函数中的类型XX*。[...]


我相信“转换后”部分之后的准参数类型应该是“ foo”而不是“ test”。
ryaner 2011年

@ryaner:很好,谢谢。虽然不是参数,但函数的类标识符是错误的。:)
Xeo

哎呀对不起我忘了玩具类名为test的时候,我读了一部分,以为f为包含foo中因此我的意见..
ryaner

可以使用构造函数完成此操作MyType(int a, double b) &&吗?
赫尔曼DIAGO

2
“ *这永远不会改变的类型”您可能应该更清楚一点,它不会根据R / L值限定而改变。但是它可以在const / non-const之间改变。
xaxxon

78

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

现在,运算符的工作方式类似于内置类型,仅接受左值。


28

假设您在一个类上有两个函数,它们的名称和签名都相同。但是其中之一被声明为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

2
我认为您需要std::move第二版,不是吗?另外,为什么右值引用返回?
Xeo

1
@Xeo:因为这就是提案中的示例;我不知道它是否仍适用于当前版本。r值参考返回的原因是,该运动应取决于捕获它的人。万一他真的想将其存储在&&而不是值中,那还不应该发生。
Nicol Bolas

3
是的,我有点想第二个问题的原因。但是,我想知道,对临时成员或其右成员的右值引用是否会延长其寿命?我可以发誓,不久前我看到了一个关于此的问题……
Xeo

1
@Xeo:并非完全如此。重载解析将始终选择非常量版本(如果存在)。您将需要进行强制转换以获取const版本。我已经更新了该帖子以进行澄清。
Nicol Bolas

15
我想我可以解释一下,毕竟我为C ++ 11创建了此功能;)Xeo坚持认为它不会改变的类型是正确的*this,但是我可以理解混乱的根源。这是因为ref-qualifier会在重载解析和函数调用期间更改“ this”(此处是故意使用的!)对象所绑定的隐式(或“ hidden”)函数参数的类型。因此,*this由于Xeo对此进行了修复,因此没有更改。相反,“hiddden”参数的改变,使其lvalue-或右值引用,就像const函数限定符使它const
bronekk
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.