传递引用、常量引用、右值引用还是常量右值引用?

12

我正在学习关于通过引用传递参数的知识,以下是我的测试结果:

#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. 我期望得到一个“不能改变s3”的结果,但为什么内部作用域中的b可以改变s3?如果将新对象s3分配给b是分配新引用,那么为什么当我将s4分配给它时,s3被更改并且s4后来为空?

抱歉问了这么多问题…当所有问题都得到回答时,我会增加奖励点数 :) 引用让我从指针的困惑进一步升级。


我不知道如何增加奖励点数…因此将等待2天直到有资格进行悬赏,然后选择答案。


7
C++ 中没有“引用的引用”。&& 语法要么是指向右值引用,要么是通用引用(在模板中)。 - dyp
通过引用引用,您指的是右值引用。标准明确指出不允许引用引用。仅供参考。C++11 § 8.3.2,p5:“不得有对引用的引用,没有引用数组,也没有引用的指针。……”为了简洁起见进行了缩写。 - WhozCraig
@DyP 是的,尽管通用引用实际上并不存在。 ;) - Yakk - Adam Nevraumont
3
不,它没有触发常量右值引用。你的代码从未触发过常量右值引用。它总是触发非常量右值引用。 - Mooing Duck
非常量引用并不违法。带有const的右值引用听起来有点奇怪。 - BЈовић
@MooingDuck 又一个打字错误。已更正。 - SwiftMango
5个回答

11

首先是代码

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&const string&传递,取决于它们是否为常量。它们也可以被复制为string。临时变量将作为string&&传递,但也可以被复制为string。有方法可以触发const string&&,但没有理由这样做,所以无关紧要。 这里展示了它们

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

标准明确规定,只有const string&string&&可以延长临时变量的生命周期,虽然我不确定为什么他们没有提到string&const string&&

我期望会出现无法更改s3,但是为什么内部作用域中的b可以更改s3?如果将一个新对象s3分配给b是分配一个新引用,为什么当我将s4分配给它时,s3被更改了,而s4之后为空?

您将b初始化为对s3的引用。不是副本,而是引用。这意味着无论如何b现在都指向s3b =“ b changed after assigning s3 \ n”;s3 =“ b changed after assigning s3 \ n”;完全相同。当您键入b = s4;时,那正好就是s3 = s4。这就是引用。它们不能被重新分配。


4
我不认为这会被视为 SOF 规则下的建设性评论,但我必须说我通过你的回答 @Mooing Duck 学到并重新学习了很多东西。你能以一种易于理解的方式详细解释问题。我知道不仅我有这样的感受,如果我的工作没有阻止我使用 SOF 的聊天功能,我会在更恰当的环境下表达我的谢意。但无论是直接还是间接地帮助了我,请接受我的感激之情。我的电子邮件在个人资料中,如果方便的话,我想与你聊一聊编码方面的问题。 :) - Dan
1
有一些方法可以触发const string&&,但从来没有理由这样做,所以这并不重要。哎呀,这真是令人失望。你介意解释一下如何触发吗? :) - user541686
2
@Mehrdad:我刚刚在问题中编辑了两种我立即想到的方法,虽然我相信还有其他方法。例如,在const变量上调用std::move可能会起作用。三种方法都相当明确,你不太可能意外发现它们。 - Mooing Duck
1
@丹:总是很惊奇能够得到那样的反馈。你赢得了在我的个人资料上的引用:D - Mooing Duck
既然我们已经有了常量引用,为什么还需要右值引用? - SwiftMango

7
可以绑定到和,例如:
void foo(const string&);
void bar(string&&);

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

但是rvalue不能绑定到非const lvalue引用。重载决议更倾向于将临时变量绑定到rvalue-refs上,而不是将它们绑定到const lvalue引用上:

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

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

lvalues只能绑定到lvalue引用。然而,请注意,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将lvalue绑定到rvalue引用:

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

这是因为rvalue的概念是可以循环利用其内容,例如声明对其动态分配的内存的所有权。如果操作后仍然可以访问该对象,则可能会存在危险,因此需要显式使用std::move来处理lvalues。


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没有触发它)?
一个常量表达式只能包含(左值到右值的转换)声明为constexpr或const的对象或rvalue临时变量。因此,据我所知,常量表达式不能产生非const左值。
为什么非常量引用和常量右值引用是非法的?
实际上两者都是允许的。虽然对我来说const右值引用没有任何意义,但您也可以使用const左值引用。
我期望的是无法更改s3,但为什么内部作用域中的b可以更改s3?如果将新对象s3分配给b相当于分配新引用,那么当我将s4分配给它并且s3被更改后s4为空,为什么会这样?
我认为您混淆了引用的初始化和分配给您声明为引用的名称的区别。

4

仅回答这部分:

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

当你使用一个常量类型的右值调用时,将会触发常量右值引用重载:

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&& 的函数是没有用的,因为你不能从一个常量中移动。它们可以作为已删除的函数有用,以防止某些调用编译。


3
如果我们传递常量表达式,它会始终触发非常量右值引用吗?什么情况下会触发常量右值引用(s6为什么不会触发)?
对于常量表达式来说,不会触发常量右值引用。唯一一种绑定到 `const&&` 的情况是它已经是 `const` 了。即使是这种情况,如果它是一个变量,则需要显式转换(见下文)。
为什么非常量引用和常量右值引用都是非法的?
我假设你指的是这些:
//string& s{""};
//paramCheck(s);

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

第一个是非法的,因为“”不是std::string变量。因此,它必须从“”构造一个std::string临时变量。s是对现有字符串变量的非const引用。您不能将非const引用取自临时变量,因为临时变量不是变量。
第二个是非法的,因为(忽略s1不存在这一事实)C++不允许您在没有显式转换的情况下获取对变量的r值引用。这就是std::move的作用。const string &&s{std::move(s3)}可以正常工作。
我原以为无法更改s3,但是为什么内部作用域中的b可以更改s3?如果将新对象s3分配给b是分配新引用,那么为什么当我将s4分配给它时,s3会发生更改,而s4之后为空?
首先,您可以轻松更改s3。b是对s3的引用;它们是同一对象的两个名称。至于其余部分,在创建b之后,您无法更改由b引用的对象。b开始引用s3,因此它将始终如此。因此,b = s4表示将s4复制到由b引用的任何对象中,即s3。
s4之后为空,因为它始终为空。您将空字符串赋值给它。所以它是空的。

2
您应该停止将Foo&&视为右值引用,而是考虑绑定到哪些事物上。
只有绑定到临时的Foo或标记为临时的Foo的函数才会使用Foo&&
这种临时标记并不持久。如果你有一个变量Foo&& foo,并使用它,它在使用时不会被标记为临时。标记某些东西为临时只能立即发生,即由返回Foo&&的函数或返回匿名Foo的函数在其即时使用中被视为临时。
将数据标记为临时的标准方法是:(A)它是临时的匿名实例Foo,(B)你在Foo实例上调用了std::move,(C)你在Foo实例上调用了std::forward<Foo>
实际上,&&既被所谓的通用引用使用,也被你想要绑定到临时的引用使用。在类型推导上下文中,左值引用可以通过将T转换为Foo&来存储在T&&中,即左值引用“胜出”右值引用。这是你需要调用std::forward来有条件地移动的情况。
简而言之:使用&&的四个常见有效位置如下:
1.当您在函数或方法参数列表中获取一个要移动的参数时。 2.当您在模板函数的参数中使用完美转发和通用引用技术时。 3.当您将完美转发的参数传递到返回值中时。 4.当您在函数范围内使用通用引用技术创建对可能为临时的引用(例如:for(auto&& i:x))时。
使用命名的&&变量时,它的行为几乎与&const &变量完全相同。为了以被视为临时的方式使用它,您需要std::move,或者在通用引用上下文中,使用std::forward有条件地std::move

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接