什么是*#34;左右参考* this"?

时间:2011-12-22 22:47:58

标签: c++ c++11 move-semantics c++-faq qualifiers

在clang的C++11 status page中遇到了一个名为“* this的右值参考”的提案。

我已经阅读了很多关于rvalue引用并理解它们的内容,但我认为我并不知道这一点。我也无法使用这些术语在网上找到太多资源。

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

这个功能是什么?

3 个答案:

答案 0 :(得分:281)

首先,"参考资格赛* 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

完成所有操作是为了让您在调用函数的对象是rvalue(例如,未命名的临时对象)时利用这一事实。请将以下代码作为进一步的示例:

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)和参考资格&&&)。


注意:此后有许多标准引号和重载解析说明!

†了解这是如何运作的,以及@Nicol Bolas&#39;答案至少部分是错误的,我们必须深入研究C ++标准(解释为什么@Nicol的答案是错误的部分在底部,如果你只对此感兴趣)。< / p>

将调用哪个函数由名为重载决策的进程决定。这个过程相当复杂,所以我们只会触及对我们很重要的位。

首先,了解成员函数的重载分辨率如何工作非常重要:

§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;

这就是成员和非成员函数包含在所谓的重载集中的原因。为了使分辨率更简单,标准报价的粗体部分存在。另外,这对我们来说很重要(同样的条款):

  

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

     
      
  • “对于没有 ref-qualifier X 声明的函数的 cv &的左值引用” REF-限定符

  •   对于使用X ref-qualifier

    声明的函数,
  • “对 cv &&的右值引用”

  •   
     

其中X是函数所属的类, cv 是成员函数声明的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

如果在测试集合中的所有重载之后,只剩下一个重载,则重载解析成功并且调用链接到该转换过载的函数。第二次打电话给&#39; f&#39;:

也是如此
// 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 匹配rvalue(仍然§13.3.1):

  

p5 [...]对于没有 ref-qualifier 声明的非静态成员函数,需要附加规则:

     
      
  • 即使隐式对象参数不是const - 限定,只要在所有其他方面可以将参数转换为隐式对象参数的类型,就可以将rvalue绑定到参数。
  •   
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*。 [...]

答案 1 :(得分:77)

左值ref-qualifier表单还有一个额外的用例。 C ++ 98的语言允许为作为rvalues的类实例调用非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...

Lvalue ref-qualifiers解决了这些问题:

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

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

答案 2 :(得分:27)

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

void SomeFunc();
void SomeFunc() &&;

你必须这样做:

void SomeFunc() &;
void SomeFunc() &&;

请注意,此声明会更改*this的类型。这意味着&&版本都将访问成员作为r值引用。因此可以轻松地从对象内移动。第一个版本的提案中给出的例子是(注意:以下内容可能与C ++ 11的最终版本不一致;它直接来自此&#34; r值来自此&#34; ;提案):

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