Moving, “RValue References”

“Return Object” Problem: Lifetime (1)

Whole class of problems: lifetime of returned objects

const std::string& f() {
    std::string s{"blah"};
    return s;
}
const std::string& f() {
    return "blah";
}

“Return Object” Problem: Lifetime (2)

const std::string& f() {
    std::string s{"blah"};
    return s;
}
  • Object’s home is on the stack

  • Returning reference to it

  • ⟶ “undefined behavior”

  • Fortunately compilers can detect and warn

warning: reference to local variable ‘s’ returned
     std::string s{"blah"};
              ^

“Return Object” Problem: Lifetime (3)

const std::string& f() {
    return "blah";
}
  • C string converted to std::string to match return type

    • Return type being reference is irrelevant

  • temporary object

  • ⟶ “undefined behavior”

warning: returning reference to temporary
     return "blah";
         ^

“Return Object” Problem: Lifetime (4)

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

    • Though std::string objects are usually reference counted (but not by standard)

    • ⟶ Cheap copy

“Return Object” Problem: Performance

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

  • Perfectly readable

  • It’s just that arrays of 100000 elements aren’t copied so lightly

  • Enter Rvalue References

#include <string>
#include <vector>


const std::string& returns_temporary()
{
    return "blah";
}

const std::string& returns_stackobject()
{
    std::string s{"blah"};
    return s;
}

std::string returns_copy()
{
    std::string s{"blah"};
    return s;
}

void output_parameter(std::string& s)
{
    s = "blah";
}

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

int main()
{
    std::string s;
    s = returns_temporary();
    s = returns_stackobject();
    s = returns_copy();
    output_parameter(s);
    auto v = returns_vector_by_copy();
}

Move Semantics: Wish List

Wish list:

  • Copy/assignment as before

  • Special constructor for moving

  • Can that be implemented in C++03?

    • Idea: non-const reference

Exercise

  • Write a class X that carries an array of int and implements the usual copy semantics and a proper destructor.

  • Additionally, for performance, the class provides a constructor that transfers ownership of the owned buffer.

  • Try out the scenarios above, and see what’s to be done in order for the move constructor to (not) be called.

Move Semantics, in C++03

Clumsy, isn’t it?

  • Constructor with non-const reference preferred over const

  • ⟶ Have to be explicit when moving is not wanted - which is the regular case!

#include <iostream>
#include <cstring>


class X
{
public:
    X(size_t size)
    : data(new int[size]),
      size(size)
    {
        std::cerr << "X(size_t)" << std::endl;
    }

    X(const X& x)
    : X(x.size)
    {
        std::cerr << "X(const X&)" << std::endl;
        memcpy(data, x.data, sizeof(int)*x.size);
    }
    
    X(X& x) 
    : data(x.data),
      size(x.size)
    {
        std::cerr << "X(X&)" << std::endl;
        x.data = 0;
        x.size = 0;
    }
    ~X()
    { 
        std::cout << "~X(): data=" << data << std::endl; 
        if (data != 0)
            delete[] data;
    }

    int *data;
    size_t size;
};

X f()
{
    X x(100);
    return x;
}

int main()
{
    std::cerr << "*** init from f() (sigh: RVO)" << std::endl;
    {
        X x = f();
    }
    std::cerr << "*** init from non-const" << std::endl;
    {
        X x(100);
        X y(x); // non-const -> moved -> x invalid
    }
    std::cerr << "*** init from const" << std::endl;
    {
        const X x(100);
        X y(x); // const (no other choice)
    }
}
  • In none of these use cases (except for function return) I want moving!

  • Function return is optimized away ⟶ Return Value Optimization (RVO)

Lvalues and Rvalues (1)

int a = 42;
int b = 43;

a = b; // ok
b = a; // ok
a = a * b; // ok

int c = a * b; // ok
a * b = 42; // error, assignment to rvalue

Lvalues and Rvalues (2)

Rules

  • Everything that has a name is an Lvalue

  • Everything that I can assign to is an Lvalue

  • Everything that I can take the address of is an Lvalue

  • Everything else is an Rvalue

So

  • Temporaries are clearly Rvalues

  • As are function calls

Moving (1)

To make the long story short …

  • Compiler will call X(X&&) when an Rvalue is passed

  • E.g. function return

  • Rules are far more complicated

  • … as is the language

  • (How about RVO?)

struct X
{
    X(X&& x)
    : data(x.data),
      size(x.size)
    {
        x.data = 0;
        x.size = 0;
    }

    int *data;
    size_t size;
};

Moving (2)

Compiler will DWIM

  • Return “by copy”

    • Select X(X&&)

    • Or RVO with copy ctor

    X f()
    {
      return X{"abc"};
    }
    X x = f();
    
  • Ordinary initialization

    • Select X(const X&)

    X x{"abc"};
    X y = x;
    

Moving (3)

Explicitly requesting move operation

X y = std::move(x);
  • std::move does not do anything the CPU must know

  • Casts to && to force selection of move-ctor

  • Usage: std::sort, for example

    • Rearrange items

    • ⟶ Copy or move, depending on what’s there

No C++ Without Pitfalls

Compiler selects function based upon parameter type

  • Normal overload selection

  • Once called, the parameter is an lvalue

  • Careful with moving

Bad

X(X&& x)
: s_(x.s_) {}

Good

X(X&& x)
: s_(std::move(x.s_)) {}