【问题标题】:Pass by reference, constant reference, rvalue-reference, or constant rvalue-reference?通过引用、常量引用、右值引用或常量右值引用传递?
【发布时间】:2013-08-01 13:55:15
【问题描述】:

我正在学习通过引用传递,这是我所做的测试:

#include <iostream>

using namespace std;

int i = 0;

//If this is uncommented, compiler gives ambiguous definition error.
//void paramCheck (string s) {
//  cout << ++i << ". Param is var.\n";
//}

void paramCheck (const string& s) {
    cout << ++i << ". Param is const ref.\n";
}

void paramCheck (string& s) {
    cout << ++i  << ". Param is non-const ref.\n";
}

void paramCheck (const string&& s) {
    cout << ++i  << ". Param is const rvalue-reference.\n";
}

void paramCheck (string&& s) {
    cout << ++i  << ". Param is non-const rvalue-reference.\n";
}


int main(int argc, char **argv) {
    //Function call test
    paramCheck("");

    paramCheck(string{""});

    string s3{""};
    paramCheck(s3);

    const string s4{""};
    paramCheck(s4);

    //Illegal
    //string& s{""};
    //paramCheck(s);

    const string& s5{s3};
    paramCheck(s5);

    string&& s6{""};
    paramCheck(s6);

    //Illegal
    //const string&& s{s1};
    //onstFP(s);

    //Reference test
    string a = s3;
    a = "a changed s3";
    cout << s3;

    {
    string& b = s3;
    b = "b changed after assigning s3\n";
    cout << "s3 is now " <<s3;

    b = s4;
    b = "b changed after assigning s4\n";
    cout << "s3 is now " <<s3;
    cout << "s4 is now " <<s4;
    }

    cin.get();
    return 0;
}

这是我得到的结果:

1. Param is non-const rvalue-reference.
2. Param is non-const rvalue-reference.
3. Param is non-const ref.
4. Param is const ref.
5. Param is const ref.
6. Param is non-const ref.
s3 is now b changed after assigning s3
s3 is now b changed after assigning s4
s4 is now

我的问题是:

  1. 如果我们传递一个常量表达式,它总是触发非常量右值引用?在什么情况下它会触发常量右值引用(为什么 s6 不触发它?)

  2. 为什么非常量引用和常量右值引用是非法的?

  3. 我原以为a不能改变s3,但是为什么内部作用域的b可以改变s3呢?如果将一个新对象 s3 分配给 b 是分配一个新引用,为什么当我将 s4 分配给它并且 s3 被更改并且之后 s4 为空时?

抱歉问了太多问题...当所有问题都回答完毕后,我会加分:) 引用只是将我的困惑从指针带到了一个全新的水平。


我不知道如何增加积分...所以将等待 2 天,直到有资格获得赏金然后选择答案。

【问题讨论】:

  • C++ 中没有引用的引用&amp;&amp; 语法指的是右值引用或通用引用(在模板中)。
  • 引用引用是指右值引用。该标准特别指出不允许引用引用。仅供参考。 C++11 § 8.3.2, p5: “不得有对引用的引用,不得有引用数组,也不得有指向引用的指针。...” 缩写为.. 好.. 简洁.
  • @DyP 是的,尽管通用引用实际上并不存在。 ;)
  • "它总是触发常量右值引用" 不,它没有,你的代码从不触发了常量右值引用。它总是触发非常量右值引用。
  • 非常量引用不是非法的。 const r-value 听起来有点奇怪

标签: c++ c++11 parameter-passing rvalue-reference


【解决方案1】:

右值可以绑定到右值引用和 const 左值引用,例如

void foo(const string&);
void bar(string&&);

foo(string{});
bar(string{});

但是右值不能绑定到非常量左值引用。重载解析更喜欢将临时对象绑定到 rvalue-refs,而不是将它们绑定到 const lvalue refs:

void foo(const string&);
void foo(string&&);

foo(string{});           // will call the second overload

左值只能绑定到左值引用。但是请注意,const 对此进行了限制:

const string do_not_modify_me;
string& modify_me = do_not_modify_me;  // not allowed, because `do_not_modify_me`
modify_me += "modified";               // shall not be modified: declared as `const`

您也可以std::move 左值将它们绑定到右值引用:

string s;
string&& r = std::move(s);

这是因为右值的概念是你可以回收它的内容,例如声明它动态分配的内存的所有权。如果您在操作后仍然可以访问对象,这可能会很危险,因此左值需要显式 std::move


paramCheck("");         // a string literal is an lvalue (!)
                        // see [expr.prim.general]/1
                        // but it is implicitly converted to a `std::string`,
                        // creating a `string` temporary, a rvalue

paramCheck(string{""}); // a temporary is an rvalue

string s3{""};
paramCheck(s3);         // the variable `s3` is an lvalue of type `string`

const string s4{""};
paramCheck(s4);         // the variable `s4` is an lvalue of type `const string`

//Illegal
//string& s{""};        // can't bind a temporary to a non-const lvalue ref
//paramCheck(s);

const string& s5{s3};
paramCheck(s5);         // the variable `s5` is a lvalue of type `const string`

string&& s6{""};        // binding a temporary to a rvalue-ref (allowed)
paramCheck(s6);         // the variable `s6` is an lvalue (!) - it has a name

//Illegal
//const string&& s{s1}; // `s1` has not been declared
//onstFP(s);

//Reference test
string a = s3;          // copy the contents of `s3` to a new string `a`
a = "a changed s3";     // overwrite contents of `a`
cout << s3;

{
string& b = s3;         // `b` refers to `s3` now (like an alias)
b = "b changed after assigning s3\n";
cout << "s3 is now " <<s3;

b = s4;                 // copy the contents of `s4` to `b` (i.e. to `s3`)
b = "b changed after assigning s4\n";
cout << "s3 is now " <<s3;
cout << "s4 is now " <<s4;
}

如果我们传递一个常量表达式,它总是触发非常量右值引用?在什么情况下它会触发常量右值引用(以及为什么 s6 不触发它?)

常量表达式只能包含声明为constexprconst 的对象(左值到右值的转换),或者是右值的临时对象。因此,AFAIK,常量表达式不能产生非常量左值。


为什么非常量引用和常量右值引用是非法的?

实际上,两者都是允许的。虽然const rvalue refs 对我没有任何意义,但您也可以使用const lvalue-refs。


我原以为a不能改变s3,但是为什么内部作用域的b可以改变s3呢?如果将一个新对象 s3 分配给 b 是分配一个新引用,为什么当我将 s4 分配给它并且 s3 被更改并且之后 s4 为空时?

我认为您对引用的初始化和分配给您声明为引用的名称之间的区别感到困惑。

【讨论】:

    【解决方案2】:

    如果我们传递一个常量表达式,它总是触发非常量右值引用?在什么情况下它会触发常量右值引用(以及为什么 s6 不触发它?)

    对于常量表达式?没有。绑定到const&amp;&amp; 的唯一时间是它已经是const。即使这样,如果它是一个变量,也需要显式转换(见下文)。

    为什么非常量引用和常量右值引用是非法的?

    我假设你在谈论这些:

    //string& s{""};
    //paramCheck(s);
    
    //const string&& s{s1};
    //onstFP(s);
    

    第一个是非法的,因为"" 不是std::string 变量。因此,它必须从"" 构造一个std::string 临时。 s 是对现有字符串variable 的非常量引用。您不能对临时对象进行非常量引用,因为临时对象不是变量。

    第二个是非法的,因为(忽略s1 不存在的事实)C++ 不允许您在没有显式 转换的情况下获得对变量的r 值引用。这就是std::move 的用途。 const string &amp;&amp;s{std::move(s3)} 工作正常。

    我原以为a不能改变s3,但是为什么内部作用域的b可以改变s3呢?如果将一个新对象 s3 分配给 b 是分配一个新引用,为什么当我将 s4 分配给它并且 s3 被更改并且之后 s4 为空时?

    首先,您可以更改s3 就好了。 bs3引用;它们是同一个对象的两个名称。至于其余的,在创建b 之后,您无法更改b 引用的对象。 b 开始引用 s3,因此它总是会这样做。因此b = s4 意味着将s4 复制到b 引用的任何对象中,即s3

    s4 之后是空的,因为它一直是空的。您为它分配了 空字符串。所以它是空的。

    【讨论】:

      【解决方案3】:

      首先是代码

      paramCheck(""); //constructs a temporary. temporaries bind to `string&&`
      paramCheck(string{""}); //constructs a temporary. temporaries bind to `string&&`
      string s3{""};
      paramCheck(s3); //passes a reference to an existing string: `string&`
      const string s4{""};
      paramCheck(s4); //passes a reference to an existing string+const: `const string&`
      //Illegal
      //string& s{""}; //cannot assign a temporary to a non-const l-reference
                       //what would s refer to when the temporary "dies"?
                       //`const string&` would have worked though
      //paramCheck(s); //passes a reference to an existing string+const: `const string&`
      const string& s5{s3}; //s5 is s3, but with `const`. 
      paramCheck(s5); //passes a reference to an existing string+const: `const string&`
      string&& s6{""}; //r-references extend the life of temporaries.
      paramCheck(s6); //passes a reference to an existing strong: `string&`
      //const string&& s{s1}; //temporaries can be extended by `T&&` or `const T&` only.
      
      //Reference test
      string a = s3; //a is a _copy_ of s3
      a = "a changed s3"; //so changing the copy doesn't effect the origional.
      cout << s3; //s3 is still blank, it hasn't changed.
      
      {
      string& b = s3; //b isn't really a "reference" to `s3`".  `b` _IS_ `s3`.
      b = "b changed after assigning s3\n"; //since `b` IS `s3`, this changes `s3`.
      cout << "s3 is now " <<s3;
      
      b = s4; //`b` _IS_ `s3`, so you just changed `s3` again.
      b = "b changed after assigning s4\n";
      cout << "s3 is now " <<s3;
      cout << "s4 is now " <<s4; //s4 is still blank, it hasn't changed.
      }
      

      然后是问题:

      如果我们传递一个常量表达式,它总是触发非常量右值引用?在什么情况下它会触发常量右值引用(以及为什么 s6 不触发它?)

      现有对象将作为 string&amp;const string&amp; 传递,具体取决于它们是否为 const。它们也可以复制为string。临时文件将作为string&amp;&amp; 传递,但也可以作为string 复制。 种方法可以触发const string&amp;&amp;,但没有理由这样做,所以没关系。 They're shown here.

      为什么非常量引用和常量右值引用是非法的?

      标准明确规定只有const string&amp;string&amp;&amp; 可以延长临时工的寿命,但我不确定他们为什么没有提及string&amp;const string&amp;&amp;

      我原以为a不能改变s3,但是为什么内部作用域的b可以改变s3呢?如果将一个新对象 s3 分配给 b 是分配一个新引用,为什么当我将 s4 分配给它并且 s3 被更改并且之后 s4 为空时?

      您将b 初始化为对s3 的引用。不是副本,而是参考。这意味着b 现在指代s3永远,无论如何。当您输入b = "b changed after assigning s3\n"; 时,它与s3 = "b changed after assigning s3\n"; 完全相同。当您键入b = s4; 时,它与s3 = s4 完全相同。这就是参考。它们不能被“重新安装”。

      【讨论】:

      • 根据 SOF 规则,我觉得这不会被视为建设性评论,但我需要说我喜欢通过你的回答来学习和重新学习东西 @Mooing Duck。你以一种对我来说有意义的方式充分解释了事情。我知道我不是唯一一个有这种感觉的人,如果我的工作没有阻止 SOF 的聊天功能,我会在更合适的环境中做到这一点。但是感谢您给我的帮助,无论是否直接。我的个人资料中有我的电子邮件,如果你愿意的话,我想和你聊聊编码。 :)
      • “有一些方法可以触发const string&amp;&amp;,但从来没有理由这样做,所以没关系。” 伙计,这太让人失望了。你介意解释一下如何吗? :)
      • @Mehrdad:编辑了立即想到问题的两种方式,但我确信还有其他方式。例如,在 const 变量上调用 std::move 可能有效。这三种方式都很明确,你可能不会偶然发现它们
      • @MooingDuck:酷,+1。
      • @Dan:得到这样的反馈总是令人惊奇。您已经在我的个人资料中获得了报价:D
      【解决方案4】:

      您应该停止将Foo&amp;&amp; 视为右值引用。想想事物绑定到什么。

      采用Foo&amp;&amp; 的函数只会绑定到临时的Foos,或标记为临时的Foos。

      这种临时标记不会持续。如果您有一个变量Foo&amp;&amp; foo,并且您使用了它,那么在使用时它不会被标记为临时变量。将某事标记为临时只能立即发生——通过返回Foo&amp;&amp; 的函数,或通过返回匿名Foo,在立即使用时,它被认为是临时的。

      将数据标记为临时数据的标准方法是 (A) 它是 Foo 的一个匿名实例,它是临时的,(B) 你在 Foo 的实例上调用了 std::move,(C) 你调用了std::forward&lt;Foo&gt;Foo 的实例上。

      在实践中,&amp;&amp; 既可用于所谓的通用引用,也可用于您要绑定到临时对象的引用。在类型推导上下文中,左值引用可以存储在T&amp;&amp; 中,方法是将T 转换为Foo&amp;——左值引用“胜过”右值引用。在这种情况下,您需要调用 std::forward 才能有条件地移动。

      简而言之:有四个常见的有效位置可以使用&amp;&amp;

      • 当您在函数或方法参数列表中获取要移动的参数时。
      • 当您在template 函数的参数中使用完美转发和通用引用技术时。
      • 当您将完美的转发参数传递到返回值时。
      • 当您使用通用引用技术在函数范围内创建对可能临时的引用时(例如,for(auto&amp;&amp; i:x))。

      当使用命名的&amp;&amp; 变量时,它的行为几乎与&amp;const &amp; 变量完全相同。为了以一种被视为临时的方式使用它,您需要std::move,或者在通用引用上下文中,使用std::forward 有条件地使用std::move

      【讨论】:

        【解决方案5】:

        只是回答这部分:

        什么情况下会触发常量右值引用

        当您使用常量类型的右值调用它时,将使用常量右值引用重载:

        void paramCheck (const string&& s) {
            cout << ++i  << ". Param is const rvalue-reference.\n";
        }
        
        const std::string functionThatReturnsConstantRvalue() { return ""; }
        
        // ...
        
        paramCheck( functionThatReturnsConstantRvalue() );
        
        const std::string s;
        paramCheck( std::move(s) );
        

        一般来说,采用const X&amp;&amp; 的函数是没有用的,因为你不能从一个常量中移动。它们可以用作已删除的函数,以防止某些调用编译。

        【讨论】:

          猜你喜欢
          • 2013-09-10
          • 2014-03-03
          • 2011-06-20
          • 1970-01-01
          • 2023-03-16
          • 2015-03-28
          • 1970-01-01
          • 1970-01-01
          • 2011-03-14
          相关资源
          最近更新 更多