Lvalues, Rvalues, And Moving

Return Object Problem: Reference To Stack-Based Object

Whole class of problems: lifetime of returned objects

const std::string& f() {
    std::string s{"blah"};
    return s;
}
warning: reference to local variable ‘s’ returned
     std::string s{"blah"};
       ^
const std::string& f() {
    return "blah";
}
warning: returning reference to temporary
     return "blah";
         ^
  • C string converted to std::string to match return type

  • Object’s home is on the stack

  • Returning reference to it

  • ⟶ “undefined behavior”

  • Fortunately compilers can detect and warn

Return Object Problem: Solution: Copy

Solution: return by copy

std::string f() {
    return "blah";
}
  • Before return, construct temporary from "blah"

  • During return, copy-construct receiver object

  • After return (during stack frame cleanup), destroy temporary

  • Performance

std::vector<int> f() {
    std::vector<int> v;
    int i=100000;
    while (i--)
        v.push_back(i);
    return v;
}

Move Semantics: Wish List

  • Copy is expensive ⟶ want cheap move instead

What if the compiler could help out?

  • Compiler should do that for me (he knows that the local variable is not used anymore after return)

  • Determine automatically that an object cannot be used anymore

  • If so, call a special kind of constructor (move constructor) that takes ownership

  • Otherwise, insert copy constructor as usual

Theory: Lvalues

Basically …

  • Anything that can be assigned to

  • Anything that has an address

  • Anything that can be dereferenced

int a = 42;
int b = 43;

a = b;
b = a;
a = a * b;

char* buffer = new char[1];
*buffer = 'a';              // <--- some expressions are lvalues
buffer[0] = 'a';            // <--- some expressions are lvalues

a * b = 42;                 // <--- some are not (error)

Theory: Rvalues

Basically …

  • Anything that is not an lvalue is an rvalue

    • Literal constants

      42 = 666;  // <--- error
      
    • This is ok though (because … ?)

      std::string("blah") = "blech";
      
    • Sadly, this is ok too (because … ?)

      std::string f();
      f() = "blech";                  // <--- ok
      

      Scott Meyers (Effective C++) says you shoud declare copy-returns as const to make it consistent 🐷

      const std::string f();
      f() = "blech";                  // <--- error
      
    • This is intended

      std::string& f();
      f() = "blech";                  // <--- ok
      
    • Results of built-in operators that aren’t lvalues

      int a = 1, b = 2;
      a + b = 666;                    // <--- error
      

Lvalue References (The Good Old Ones)

  • Non-const lvalue references can only bind to lvalues

    int a;
    int& b = a;             // <--- ok
    int& c = a * 2;         // <--- error: cannot bind to rvalue
    
  • Const lvalue references can bind to lvalue or rvalues

    const int& r1 = a * 2;         // <--- ok: bind to rvalue
    const std::string& r2 = f();   // <--- ok: bind to rvalue
    

    ⟶ Non-trivial: compiler has to set aside storage to outlive the reference

Rvalue References: So What Is That?

  • Double ampersand &&

  • Rvalue references can only bind to rvalues

  • And can be assigned to!

    int&& rvr = 42;   // <--- bound to an rvalue (an int literal)
    rvr = 7;          // <--- assigned to!
    
  • Cannot bind to lvalues

    int a = 666;
    int&& rvr = a;    // <--- error: cannot bind rvalue reference to lvalue
    
  • But rvalue references just don’t make sense as standalone variables

  • ⟶ Preferred use as function parameters for overload resolution

Rvalue References As Function Parameters

  • Semantics just the same as with standalone variables

  • Rvalue references can only bind to rvalues

void f(int&& param);

int a;
f(a);     // <--- error: cannot bind rvalue reference to lvalue
f(a*2);   // <--- ok: rvalue

Use Of Rvalue References Parameter Inside A Function: Is An Lvalue

  • Inside a function, and rvalue type parameter is an lvalue

  • It has a name, after all! And an address!

    void f(int&& param);
    void g(int&& param)
    {
        f(param);   // <--- error: cannot bind rvalue reference to lvalue
    }
    
  • Enter std::move

    #include <utility>
    
    void f(int&& param);
    void g(int&& param)
    {
        f(std::move(param));
    }
    
  • Converts lvalue to rvalue

  • ⟶ Object copy vs. object move

Enter Move Constructor And Move Assignment

  • Compiler knows that after the return of a local variable that variable cannot be used anymore

  • ⟶ inserts move constructor and/or move assignment operators

class foo
{
public:
    foo(foo&&) noexcept;
    foo& operator=(foo&&) noexcept;
};

Rules

  • Move constructor is implicitly defined by compiler as member- (and base-) wise move if there are no user-declared …

    • Destructor

    • Copy constructor

    • Copy assignment operator

    • Move assignment operator

  • Move assignment operator is implicitly defined by compiler as member- (and base-) wise move if there are no user-declared …

    • Destructor

    • Copy constructor

    • Copy assignment operator

    • Move constructor