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

若如初见. 提交于 2020-01-10 10:32:14

问题


I was learning passing by reference, and here is the test I did:

#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;
}

And here is the result I get:

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

My question is:

  1. If we pass a constant expression, it always triggers non-constant rvalue-reference? Under what condition it will trigger constant rvalue-reference (and why s6 is not trigging it?)

  2. Why non-constant reference and constant rvalue-reference are illegal?

  3. I expected a cannot change s3, but why b in the inner scope can change s3? If assigning a new object s3 to b is assigning a new reference, why when I assign s4 to it and s3 got changed and s4 is empty afterwards?

Sorry for asking too many questions... I will increase the points when all questions are answered :) The reference just brings my confusion from pointer to a whole new level.


I don't know how to increase the point... so will wait for 2 days till eligible for bounty then choose the answer.


回答1:


First the code

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.
}

Then the questions:

If we pass a constant expression, it always triggers non-constant rvalue-reference? Under what condition it will trigger constant rvalue-reference (and why s6 is not trigging it?)

Existing objects will pass as string& or const string& depending on if they're const or not. They can also be copied in as string. Temporaries will pass as string&&, but can also be copied in as string. There are ways to trigger const string&&, but there's no reason to do so ever, so it doesn't matter. They're shown here.

Why non-constant reference and constant rvalue-reference are illegal?

The standard specifically says that only const string& and string&& will extend the lives of temporaries, though I'm not certain why they didn't also mention string& and const string&&.

I expected a cannot change s3, but why b in the inner scope can change s3? If assigning a new object s3 to b is assigning a new reference, why when I assign s4 to it and s3 got changed and s4 is empty afterwards?

You initialized b as a reference to s3. Not a copy, but a reference. That means b now refers to s3 forever, no matter what. when you typed b = "b changed after assigning s3\n";, that's exactly the same as s3 = "b changed after assigning s3\n";. When you typed b = s4;, that's exactly the same as s3 = s4. That's what a reference is. They cannot be "reseated".




回答2:


rvalues can bind to rvalue references and const lvalue references, e.g.

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

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

But rvalue cannot bind to non-const lvalue references. Overload resolution prefers binding temporaries to rvalue-refs over binding them to const lvalue refs:

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

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

lvalues can only bind to lvalue references. Note, however, that const restricts this:

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`

You can std::move lvalues to bind them to rvalue references as well:

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

This is because the notion of an rvalue is that you can recycle its contents, e.g. claim ownership of the memory it has dynamically allocated. This can be dangerous if you still have access to the object after the operation, therefore the explicit std::move is required for 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;
}

If we pass a constant expression, it always triggers non-constant rvalue-reference? Under what condition it will trigger constant rvalue-reference (and why s6 is not trigging it?)

A constant expression can only contain (lvalue-to-rvalue conversions of) objects declared either constexpr or const, or temporaries, which are rvalues. Therefore, AFAIK, a constant expression cannot yield a non-const lvalue.


Why non-constant reference and constant rvalue-reference are illegal?

Both are allowed, actually. Though const rvalue refs don't make any sense to me, you can use const lvalue-refs as well.


I expected a cannot change s3, but why b in the inner scope can change s3? If assigning a new object s3 to b is assigning a new reference, why when I assign s4 to it and s3 got changed and s4 is empty afterwards?

I think you're confused about the difference between the initialization of a reference and assigning to a name you declared as a reference.




回答3:


Just to answer this part:

Under what condition it will trigger constant rvalue-reference

The constant rvalue-reference overload will be used when you call it with an rvalue of constant type:

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) );

In general functions taking a const X&& are useless, because you can't move from a constant. They can be useful as deleted functions, to prevent certain calls from compiling.




回答4:


If we pass a constant expression, it always triggers non-constant rvalue-reference? Under what condition it will trigger constant rvalue-reference (and why s6 is not trigging it?)

For a constant expression? None. The only time something will bind to const&& will be if it is already const. And even then, that will require an explicit cast if it is a variable (see below).

Why non-constant reference and constant rvalue-reference are illegal?

I'll assume you're talking about these:

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

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

The first is illegal because "" is not a std::string variable. Therefore, it must construct a std::string temporary from "". s is a non-const reference to an existing string variable. You can't take a non-const reference to a temporary, since a temporary is not a variable.

The second is illegal because (ignoring the fact that s1 does not exist) C++ does not allow you to get an r-value reference to a variable without an explicit conversion. This is what std::move is for. const string &&s{std::move(s3)} works just fine.

I expected a cannot change s3, but why b in the inner scope can change s3? If assigning a new object s3 to b is assigning a new reference, why when I assign s4 to it and s3 got changed and s4 is empty afterwards?

First, you can change s3 just fine. b is a reference to s3; they are two names for the same object. As for the rest, you cannot change what object is referenced by b after b is created. b starts out referencing s3, therefore it will always do so. Thus b = s4 means to copy s4 into whatever object is referenced by b, which is s3.

s4 is empty afterwards because it was always empty. You assigned the empty string to it. So it's empty.




回答5:


You should stop thinking of Foo&& as an rvalue reference. Think rather of what things bind to.

A function taking Foo&& will only bind to temporary Foos, or Foos marked as temporary.

This temporary marking does not last. If you have a variable Foo&& foo, and you use it, it isn't marked as temporary at the point of use. Marking something as being a temporary can only happen immediately -- by a function that returns a Foo&&, or by returning an anonymous Foo which, in its immediate use, is considered temporary.

The standard ways to mark data as temporary is to (A) it is an anonymous instance of Foo that is temporary, (B) you called std::move on an instance of Foo, (C) you called std::forward<Foo> on an instance of Foo.

In practice, && is used both by what is known as universal references, and by references you want to bind to temporaries. In a type deduction context, lvalue references can be stored in a T&& by making T into Foo& -- the lvalue reference "wins" over the rvalue reference. This is the situation where you need to call std::forward in order to conditionally move.

In short: there are four common valid spots to use &&.

  • When you take an argument you want to be moved-from in a function or method argument list.
  • When you are using perfect forwarding and the universal reference technique in a template function's arguments.
  • When you are doing a pass-through of a perfect forwarded parameter to a return value.
  • When you are doing the universal reference technique to create a reference to maybe-a-temporary in function scope (For example, for(auto&& i:x)).

When using a named && variable, it acts almost exactly like a & or const & variable. In order to use it in a way that it is treated as temporary, you need to std::move, or in a universal reference context, use std::forward to conditionally std::move.



来源:https://stackoverflow.com/questions/17980570/pass-by-reference-constant-reference-rvalue-reference-or-constant-rvalue-refe

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!