【问题标题】:What is "rvalue reference for *this"?什么是“*this 的右值引用”?
【发布时间】:2012-01-26 11:45:20
【问题描述】:

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

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

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

这个功能是关于什么的?

【问题讨论】:

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


    【解决方案1】:

    首先,“*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&amp;&amp;&amp;)。


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

    † 要了解这是如何工作的,以及为什么@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 对于非静态成员函数,隐含对象参数的类型为

    • “对 cv X 的左值引用”对于没有 ref-qualifier 或使用 &amp; ref-qualifier 声明的函数

    • “对 cv X 的右值引用”对于使用 &amp;&amp; 声明的函数 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 匹配一个右值(仍然是@ 987654341@):

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

    • 即使隐式对象参数不是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 表达式,其值是调用该函数的对象的地址。类X 的成员函数中this 的类型是X*。 [...]

    【讨论】:

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

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

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

    【讨论】:

      【解决方案3】:

      假设您在一个类上有两个函数,它们都具有相同的名称和签名。但是其中一个被声明为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-value 限定符,要么都不使用。你不能这样做:

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

      你必须这样做:

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

      请注意,此声明会更改 *this 的类型。这意味着&amp;&amp; 将所有访问成员版本都作为右值引用。因此,可以轻松地从对象内部移动。提案的第一个版本中给出的示例是(注意:以下内容可能与 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 等一样。
      猜你喜欢
      • 1970-01-01
      • 2015-12-13
      相关资源
      最近更新 更多