传递引用、常量引用、右值引用或常量右值引用

Pass by reference, constant reference, rvalue-reference, or constant rvalue-reference?

本文关键字:引用 常量      更新时间:2023-10-16

我是通过参考来学习及格的,下面是我做的测试:

#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 s3n";
    cout << "s3 is now " <<s3;
    b = s4;
    b = "b changed after assigning s4n";
    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天,直到有资格获得赏金,然后选择答案。

首先是代码

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 s3n"; //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 s4n";
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&&

我原以为a不能改变s3,但为什么b在内部范围内可以改变s3?如果将一个新对象s3分配给b就是分配一个新的引用,那么为什么我将s4分配给它,s3被更改,之后s4为空呢?

您将b初始化为对s3的引用。不是副本,而是参考资料。这意味着b现在永远引用s3,无论如何。当您键入b = "b changed after assigning s3n";时,它与s3 = "b changed after assigning s3n";完全相同。当您键入b = s4;时,它与s3 = s4完全相同。这就是引用。它们不能"重新密封"。

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

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

但是右值不能绑定到非常量的左值引用。重载解析更喜欢将临时值绑定到右值引用,而不是将它们绑定到常量左值引用:

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 lvalues将它们绑定到rvalue引用:

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

这是因为右值的概念是您可以回收其内容,例如,声明其动态分配的内存的所有权。如果在操作之后仍然可以访问对象,这可能是危险的,因此lvalues需要显式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 s3n";
cout << "s3 is now " <<s3;
b = s4;                 // copy the contents of `s4` to `b` (i.e. to `s3`)
b = "b changed after assigning s4n";
cout << "s3 is now " <<s3;
cout << "s4 is now " <<s4;
}

如果我们传递一个常量表达式,它总是触发非常量右值引用?在什么条件下,它将触发恒定值参考(为什么s6没有触发它?)

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


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

事实上,两者都是允许的。虽然const左值引用对我来说没有任何意义,但你也可以使用const左值引用。


我原以为a不能改变s3,但为什么b在内部范围内可以改变s3?如果将一个新对象s3分配给b就是分配一个新的引用,那么为什么我将s4分配给它,s3被更改,之后s4为空呢?

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

只回答这一部分:

在什么条件下,它将触发恒定值参考

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

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

如果我们传递一个常量表达式,它总是触发非常量右值引用?在什么条件下,它将触发恒定值参考(为什么s6没有触发它?)

对于常量表达式?没有一个只有当某个东西已经是const时,它才会绑定到const&&。即便如此,如果它是一个变量,也需要显式强制转换(见下文)。

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

我想你说的是这些:

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

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

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

我原以为a不能改变s3,但为什么b在内部范围内可以改变s3?如果将一个新对象s3分配给b就是分配一个新的引用,那么为什么我将s4分配给它,s3被更改,之后s4为空呢?

首先,您可以很好地更改s3b是对s3引用;它们是同一对象的两个名称。至于其他对象,在创建b之后,不能更改b引用的对象。b从引用s3开始,因此它将始终这样做。因此b = s4意味着将s4复制到b引用的任何对象中,即s3

s4后来是空的,因为它总是空。你给它分配了空字符串。所以它是空的。

您应该停止将Foo&&视为右值引用。想一想事物的本质。

采用Foo&&的函数将仅绑定到临时Foo s或标记为临时的Foo s。

这种临时标记不会持久。如果您有一个变量Foo&& foo,并且您使用了它,那么在使用时它不会被标记为临时的。将某个东西标记为临时的只能立即发生——通过返回Foo&&的函数,或者通过返回匿名Foo(在其立即使用时被视为临时的)。

将数据标记为临时的标准方法是:(A)它是临时的Foo的匿名实例,(B)您在Foo的实例上调用了std::move,(C)在Foo的实例上使用了std::forward<Foo>

在实践中,&&既被称为通用引用的引用使用,也被要绑定到临时引用的引用所使用。在类型推导上下文中,通过将T转换为Foo&,可以将左值引用存储在T&&中——左值引用"战胜"了右值引用。在这种情况下,您需要调用std::forward才能有条件地移动。

简而言之:&&共有四个常用的有效点。

  • 获取函数或方法参数列表中要从中移出的参数时
  • 当您在template函数的参数中使用完美转发和通用引用技术时
  • 当您将一个完美的转发参数传递给一个返回值时
  • 当您使用通用引用技术创建引用时,该引用可能是函数范围中的临时引用(例如,for(auto&& i:x)

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