Table of Contents

  1. Lvalues and Rvalues
  2. Lvalue and Rvalue Reference
  3. std::move and std::forward
  4. Moving Objects

(1) Lvalues and Rvalues ↑top

Every expression in C++ is either an rvalue or an lvalue. Roughly speaking, when we use an obj as an rvalue, we use the obj's value (its contents). When we use an obj as an lvalue, we use the obj's identity (its location in memory).

Operators differ as to whether they require lvalue or rvalue operands and as to whether they return lvalues or rvalues. We can use an lvalue when rvalue is required (the obj's contents/value will be used), but we cannot use an rvalue when an lvalue (i.e., a location) is required.

Lvalues and rvalues also differ when we use with decltype. When decltype is applied to an expr (other than a var), the result is a ref type if the expr yields an lvalue.

int *p;
//deref yields an lvalue
decltype(*p); //type is: int&
//addr-of yields an rvalue
decltype(&p); //type is: int**

Heuristic to determine whether an expr is an lvalue: usually is if u can take its addr. To the contrary, rvalues are values whose addr cannot be obtained by dereferencing them, either because they are literals or because they are temporary in nature (such as values returned by funcs or explicit cstr calls).

A parameter is always an lvalue, even if its type is an rvalue ref.

(2) Lvalue and Rvalue Reference ↑top

(i) lvalue/rvalue reference

We cannot bind regular ref, i.e., lvalue references, to expressions that require a conversion (two exceptions: const int &i = dval; and Base &bf = derived), to literals, or to expr that return an rvalue.

//(1) Lvalue ref aliases an existing obj
std::string s = "Ex";
std::string& r1 = s;
r1 += "ample"; //modifies s
//(2) implements pass-by-ref in func calls
void double_string(std::string& s){
    s += s;
}
std::string str = "Test";
double_string(str);
//(3) when a func's return type is lvalue ref,
// the func call expr becomes an lvalue expr
char& char_number(std::string& s, std::size_t n){
    return s.at(n); //returns a ref to char
}
std::string str = "Test";
char_number(str, 1) = 'a'; //func call is lvalue

An rvalue reference is an reference that must be bound to an rvalue. Like any ref, an rvalue ref is just an alias for an obj.

Rvalue refs have the opposite binding properties: we can bind an rvalue ref to these kinds of exprs, but we cannot directly bind an rvalue ref to an lvalue.

int i = 42;
int &r = i; //OK: r refers to i
int &&rr = i; //ERROR: cannot bind rvalue ref to an lval
int &r2 = i * 42; //ERROR: i*42 is an rvalue
const int &r3 = i * 42; //OK: can bind a ref to const to rval
int &&rr2 = i * 42; //OK: bind rr2 to the rst

When a func has both rvalue ref and lvalue ref overloads, the rvalue ref overload binds to rvalues, while the lvalue ref overload binds to lvalues.

void f(int& x){
    std::cout << "lvalue ref overload f(" << x << ")\n";
}
void f(const int& x){
    std::cout << "lvalue ref to const overload f(" << x << ")\n";
}
void f(int&& x){
        std::cout << "rvalue ref overload f(" << x << ")\n";
}

int main(){
    int i = 1;
    const int ci = 2;
    f(i); //calls f(int&)
    f(ci); //calls f(const int&)
    f(3); //calls f(int&&)
          //would call f(const int&) if no f(int&&)
}

A variable is an lvalue; we cannot directly bind an rvalue ref to a var even if that var was defined as an rvalue ref type.

(ii) universal reference

To declare an rvalue ref to some type T, we write T&&, but T&& is not always rvalue ref:

void f(Widget&& param);     //rvalue ref
Widget&& var1 = Widget();   //rvalue ref
auto&& var2 = var1;         //NOT rvalue ref
template<typename T>
void f(std::vector<T>&& param); //rvalue ref
template<typename T>
void f(T&& param);          //NOT rvalue ref

T&& has two different meanings:

(3) std::move and std::forward ↑top

Although we cannot directly bind an rvalue ref to an lvalue, we can explicitly cast an lvalue to its corresponding rvalue ref type; we can also obtain an rvalue ref bound to an lvalue by calling a new library func named move.

int&& rr1 = 42; //OK: literals are rvals, but var is lval
int&& rr3 = std::move(rr1); //OK: return rval ref 

std::move(arg) returns an rvalue reference to arg; the func return is the same as

static_cast<remove_reference<decltype(arg)>::type&&>(arg);

std::move and std::forward are merely funcs (actually func templates) that perform casts, std::move unconditionally casts its argument to an rvalue, while std::forward performs this cast only if a particular cond is fulfilled.

class Annotation {
public:
    explicit Annotation(const std::string text)
    : value(std::move(text)) //"move" text into value
    { ... }                  //code doesn't do what it seems to!
    ...
private:
    std::string value;
};

The code correctly sets the value to the contents of text, but text is not moved into value, it's copied. text is cast to rvalue by std::move, but text is declared to be const std::string, so before the cast, text is is an lvalue const std::string, and the rst of the cast is an rvalue const std::string (i.e., constness remains).

How compiler determine which std::string constructor to call:
std::move(text) returns a rvalue const std::string, which cannot be passed to std::string's move cstr, because the move cstr takes an rvalue ref to a non-const. However, the rvalue can be passed to the copy cstr, because an lvalue-ref-to-const is permitted to bind to a const value. Therefore, copy cstr is invoked, even though text has been cast to an rvalue!

class string {//std::string is actually a
public:       //typedef for std::basic_string<char>
    ...
    string(const string& rhs); //copy cstr
    string(string&& rhs);      //move cstr
    ...
};

Moving a value out of an obj generally modifies the obj, so the language should not permit const objs to be passed to funcs, e.g., move cstrs, that could modify them.

std::forward is a conditional cast:

void process(const Widget& lvalArg); //process lvalues
void process(Widget&& rvalArg);      //process rvalues

template<typename T>
void logAndProcess(T&& param) {
    auto now = std::chrono::system_clock::now();
    makeLogEntry("Calling 'process'", now);
    process(std::forward<T>(param));
}
----------
Widget w;

logAndProcess(w);            //call with lvalue
logAndProcess(std::move(w)); //call with rvalue

process is overloaded for rvalues and rvalues and we naturally expect that the rvalue to be forwarded to rvalue overload of process, but:
param is an lvalue, and every call to process inside logAndProcess will thus want to invoke the lvalue overload for process.

To prevent this, we should use the conditional-cast std::forward: it casts to an rvalue only if its arguments was initialized with an rvalue. The init info is encoded in logAndProcess's template para T, then passed to std::forward, which recovers the encoded info.

Takeaways:

(3) Moving Objects ↑top

(i) an example

void StrVec::reallocate(){
    auto newcapacity = size() ? 2*size() : 1;
    //allocate new memory
    auto newdata = alloc.allocate(newcapacity);
    //move the data from old mem to the new
    auto dest = newdata; //points to the 1st free pos in new
    auto elem = elements; //points to 1st ele in old
    
    for(size_t i=0; i!=size(); ++i)
        alloc.construct(dest++, std::move(*elem++));
    free(); //free the old space we've moved the eles
}

Calling move returns a rst that causes construct to use the string move cstr: the mem managed by those strings will not be copied; instead, each string we construct will take over ownership of the mem from the string to which elem points.

After moving the elements, we call free to destroy the old eles and free the old memory. The strings themselves no longer manage the mem to which they had pointed; responsibility for their data has been moved to the eles in the new StrVec mem. We don't know the value the strings in the old StrVec mem have, but we are guaranteed that it is safe to run the string dstr on these objs.

The library containers, string and shared_ptr classes support move as well as copy. The IO and unique_ptr classes can be moved but not copied.

Rvalue references refer to objs that are about to be destroyed. Hence, we an "steal" state from an obj bound to an rvalue ref.

(ii) Move constructors and assignment

Move cstrs and asgnment operator are similar to the crspding copy operations, but they "steal" resources from their given obj rather than copy them.

The move cstr must ensure that the moved-from obj is left in a state such that destroying the obj will be harmless. In paticular, once its resources are moved, the original obj must no longer point to those moved resources - responsibility for those resources has been assumed by the newly created obj.

StrVec::StrVec(StrVec &&s) noexcept //move won't throw
    //member initializers take over the rsc in s
    : elements (s.elements), first_free (s.first_free)
      , cap(s.cap) {
        //leave s in a state in which it is safe to run dstr
        s.elements = s.first_free = s.cap == nullptr;
    }

Move cstrs and move asgnment operators that cannot throw exceptions should be marked as noexcept (on both decl and def).

After a move operation, the "moved-from" obj must remain a valid, destructible obj but users may make no assumptions about its value.

(iii) synthesized move operations

Copy cstr and copy asgnment operator are always synthesized by the compiler, if we don't define our own. Differently, for some classes the compiler does not synthesize move operations at all: if a class defines its own copy cstr, copy-asgn operator, or dstr;

The compiler synthesizes a move cstr or a move-asgn operator only if the class doesn't define any of its own copy-control members and if every nonstatic data member of the class can be moved.

The compiler can move members of built-in type, and it can also move members of a class type if the member's class has the cspding move operation:

//the compiler will synthesize the move ops for X & hasX
struct X {
    int i;         //built-in type can be moved
    std::string s; //string defines its own move op
};
struct hasX {
    X mem;         //X has synthesized move ops
};
---------
X x, x2 = std::move(x);       //uses the syned move cstr
hasX hx, hx2 = std::move(hx); //uses syned move cstr

Classes that define a move cstr or move-asgn operator must also define their own copy operations. Ow, those members are deleted by default.

(iv) assignment and member funcs

class HasPtr {
public:
    //added move cstr
    HasPtr (HasPtr &&p) noexcept
    : ps(p.ps), i(p.i) { p.ps = 0; }
    //asgnment op is bothe the move- & copy- asgn op
    HasPtr& operator= (HasPtr rhs) {
        swap(*this, rhs); return *this;
    }
};

The asgnment operator has a nonref para, which means that the para is copy initialized. This single asgnment op acts as both the copy- and move-asgnment op. Depending on the type of the argument, copy init uses either the copy cstr or the move cstr; lvalues are copied and rvalues are moved.

hp = hp2;           //hp2 is an lvalue; copy cstr used
//the move cstr copies the ptr from hp2, no mem alloc
hp = std::move(hp2); //move cstr moves hp2

Member funcs other than cstrs and asgnment can benefit from providing both copy and move versions, one takes an lvalue reference to const, and the other takes an rvalue reference to nonconst:

class StrVec {
public:
    void push_back(const std::string&); //copy ele
    void push_back(std::string&&);      //move ele
};

void StrVec::push_back(const string& s){
    chk_n_alloc();
    //cst a copy of s
    alloc.construct(first_free++, s);
}
void StrVec::push_back(string &&s){
    chk_n_alloc();
    alloc.construct(first_free++, std::move(s));
}
-----------
StrVec vec;             //empty StrVec
string s = "some string";
vec.push_back(s);      //calls const string &
vec.push_back("done"); //calls string&&

Ordinarily, we can call a member func on an obj, regardless of whether that obj is an lvalue or an rvalue:

string s1 = "a value", s2 = "another";
auto n = (s1+s2).find('a');
--------
s1+s2 = "wow";

To prevent such usage, we'd like to force the left-hand operand to be lvalue by placing a reference qualifier after the para list:

class Foo {
public:
    //may assign only to modifiable lvalues
    Foo &operator=(const Foo&) &;
};
Foo &Foo::operator= (const Foo &rhs) &
{
    //do whatever is needed to assign rhs to this obj
    return *this;
}

The ref qualifier can be either & or &&. Like the const qualifier, a ref qualifier may appear only on a (nonstatic) member func and must appear in both the decl and def of the func.