ChatGPT解决这个技术问题 Extra ChatGPT

什么是“*this 的右值引用”?

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

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

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

这个功能是关于什么的?


C
Community

首先,“*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);
  }
};

这可能有点做作,但你应该明白。

请注意,您可以组合 cv-qualifiersconstvolatile)和 ref-qualifiers&&&)。

注意:这里后面有很多标准引号和重载解析说明!

† 要了解这是如何工作的,以及为什么@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

您当然希望以下调用免费功能,不是吗?

char const* s = "free foo!\n";
foo f;
f << s;

这就是为什么成员和非成员函数包含在所谓的重载集中的原因。为了使解决方案不那么复杂,标准引用的粗体部分存在。此外,这对我们来说很重要(相同的条款):

p4 对于非静态成员函数,隐式对象参数的类型对于没有 ref-qualifier 或带有 & ref-qualifier 声明的函数是“对 cv X 的左值引用”,对于使用&& ref-qualifier 其中 X 是函数所属的类, cv 是成员函数声明上的 cv 限定符。 [...] 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

如果在测试集合中的所有重载之后,只剩下一个,则重载解析成功并且链接到转换后的重载的函数被调用。第二次调用 '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-qualifier 声明的非静态成员函数,附加规则适用:即使隐式对象参数不是 const-qualified,只要在所有其他方面参数可以转换为隐式对象参数的类型。

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 是一个纯右值表达式,其值是调用该函数的对象的地址。类 X 的成员函数中 this 的类型是 X*。 [...]


我相信“转换后”部分之后的参数类型应该是“foo”而不是“test”。
@ryaner:很好的发现,谢谢。虽然不是参数但函数的类标识符是错误的。 :)
哎呀抱歉,当我阅读该部分并认为 f 包含在 foo 中时,我忘记了名为 test 的玩具类,因此我的评论..
这可以通过构造函数完成吗:MyType(int a, double b) &&
“*this 的类型永远不会改变”您可能应该更清楚一点,它不会基于 r/l-value 限定而改变。但它可以在 const/non-const 之间变化。
J
JohannesD

左值引用限定符形式还有一个用例。 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&) &;
};

现在操作符像内置类型一样工作,只接受左值。


N
Nicol Bolas

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

void SomeFunc() const;
void SomeFunc();

如果类实例不是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

我认为您需要std::move第二个版本,不是吗?另外,为什么右值引用返回?
@Xeo:因为这就是提案中的示例;我不知道它是否仍然适用于当前版本。 r 值参考返回的原因是因为运动应该取决于捕捉它的人。它不应该发生,以防万一他真的想将它存储在 && 而不是一个值中。
对了,我有点想到第二个问题的原因了。不过,我想知道,对临时成员的右值引用是否会延长该临时成员或其成员的生命周期?我可以发誓我前段时间在 SO 上看到过一个关于这个的问题......
@Xeo:这并不完全正确。重载解析将始终选择非常量版本(如果存在)。您需要进行强制转换才能获得 const 版本。我已经更新了帖子以澄清。
我想我可以解释一下,毕竟我为 C++11 创建了这个功能;)Xeo 坚持认为它不会改变 *this 的类型是正确的,但是我可以理解混淆的来源。这是因为 ref-qualifier 更改了在重载决策和函数调用期间绑定“this”(此处故意放引号!)对象的隐式(或“隐藏”)函数参数的类型。因此,*this 没有变化,因为 Xeo 解释说这是固定的。而是更改“隐藏”参数以使其成为左值或右值引用,就像 const 函数限定符使其成为 const 等一样。