std::move std::forward<T> into Modern C++

Erich Keane

erich.keane@intel.com

Presentation Warnings

  • Examples show some advanced C++/C++11/14/17 features.  If you're confused and it stops you from understanding the rest, ask now, else ask me later!
  • Pseudocode at times, please don't be too harsh on syntax
  • A true full-coverage of this topic would take days, I have an hour!  Some details are sacrificed
    • I may answer your questions/clarifications with 'ask me later'.  Please do!
    • Want more depth?
      • Ask questions outside of talk, I'll talk a lot!
      • Look stuff up! Read books

Why Should I Care about Move Semantics?

  • Prevents Unnecessary copying!
  • Creates faster code in many situations
  • Often Free!  Most of the STL has already implemented it, so add -std=c++11/14/1z now!
  • Can be very easy way of improving performance with very little effort!
  • std::string/std::vector/etc can now be passed around much more efficiently!
  • Enabled std::unique_ptr!

What was the problem?

  • Copying large things is expensive!  std::string/std::vector could be a giant performance hit!
  • Have you ever seen one of these?
std::string s = otherstring;
std::vector<int> vect = othervect;
std::vector<double> doubleVect = SomeFunc(args);

What happens when you copy a vector:

std::string s = otherstring;
std::vector<int> vect = othervect;
std::vector<double> doubleVect = SomeFunc(args);

namespace std {
template <typename T> // others omitted
class vector {
[...]
public:
vector(const vector<T>& other):size(other.size), 
    capacity(other.size) {
    data = (T*)malloc(sizeof(T) * size); // Expensive!
    for (size_t i = 0; i < size; ++i)
        data[i] = other[i]; // potentially expensive!
}
[...]
private:
T* data;
size_t size;
size_t capacity;
[...]
};
}
  • A copy of a vector/string requires an allocation!
  • A Copy of a vector/string requires copying everything else!
  • Example #3 has a temporary being immediately destroyed

What happens when you copy a vector:

std::vector<SomeClass> doubleVect = SomeFunc(args);

// Will Do: 
// std::vector ctor inside SomeFunc
// std::vector copy ctor to doubleVect
// std::vector dtor for the temporary
  • Cannot RVO if SomeClass' ctor/dtor are observable
  • For a moment, 2 entire copies of the vector are present
  • We copy the vector, then immediately delete it!

Enter Move Semantics

vector(vector<T>&& other) : size(other.size), // move construction
capacity(other.capacity), data(other.data) {
    other.data = nullptr;
}
vector& operator=(vector<T>&& other); // move assignment!
  • Note the loss of 'const' and additional '&'
  • Compiler calls these when the RHS is a 'temporary' (it has no name!)
  • We 'steal' the internal representation of the temporary.
  • Temporary left in a 'valid but indeterminate state'
  • "Copy-elision" possible EVEN with side effects!
  • Particularly useful when you want a "single owner" for a resource(see std::unique_ptr<T>!)

std::unique_ptr<T>

namespace std { template <typename T>class unique_ptr{
    unique_ptr(){...}; // can create empty
    unique_ptr(T* target){...}; // can create from a pointer (to own!)
    ~unique_ptr(){...}; // calls 'delete' on target
    unique_ptr(const unique_ptr& other)= delete; // Not allowed, would be multiple owners!
    unique_ptr(unique_ptr&& other){...}; // allowed, steals from 'other'
    
    unique_ptr& operator=(const unique_ptr& other) = delete;
    unique_ptr& operator=(unique_ptr&& other){...};
};}

// Can not copy 2 unique_ptrs:
unique_ptr<int> i, j;
i = j; // compile error
unique_ptr<int> k = j; // compile error
 // function def is unique_ptr<int> make_some_int()
unique_ptr<int> m = make_some_int(); // transfers ownership!

LValue vs RValue

  • LValues are things you can take the address of
    • Named Objects
    • Things you expect to exist afterwards!
    • Type& <-- "LValue Reference"
  • RValues are things you cannot take the address of
    • Unnamed temporaries
    • Type&& <-- "RValue Reference"
  • Reference Binding Rules
    • LValues may bind to LValue references
    • RValues may bind to LValue references to const
    • RValues may bind to RValue references to non-const
    • LValues may NOT bind to RValue references!

Moving Non-temporaries

  • What if you're done with something, so you want to move it, but it has a name?
    • Note: A "Parameter" is ALWAYS an LValue, even if it was bound to an RValue
    • Enter std::move: Simply a CAST to a RValue
class Foo{
    public:
     // rhs is bound to a RValue, 
     // HOWEVER, 'rhs' is a named LValue!
    Foo(Foo&& rhs) : std::move(rhs._str){}

    private: std::string _str;
};

void some_func() {
unique_ptr<int> p, q;
p = q; // compile error
p = std::move(q); // fine!
do_thing(q); // not a compile error, but DEFINITELY a bug!

Universal Reference

  • 1 Function that "does the right thing"
    • Copies LValue args, Moves RValue Args
  • When "&&" is on a template parameter, the parameter is a "Universal Reference", not an "RValue Reference"

class Foo{
    public:
     // rhs is bound to a RValue, 
     // HOWEVER, 'rhs' is a named LValue!
    Foo(Foo&& rhs) : std::move(rhs._str){}
    
    template<typename T1, typename T2>
    Foo(T1&& leftArg, T2&& rightArg){...}

    private: std::string _str;
};

How to implement?

  • Enter std::forward<T>: When passed an RValue Reference, returns an RValue.  When passed an LValue Reference, returns an LValue.

class Foo{
    public:
     // rhs is bound to a RValue, 
     // HOWEVER, 'rhs' is a named LValue!
    Foo(Foo&& rhs) : std::move(rhs._str){}
    
    template<typename T1, typename T2>
    Foo(T1&& leftArg, T2&& rightArg): 
        _str1(std::forward<T1>(leftArg), _str2(std::forward<T2>(rightArg){}
    private: std::string _str1;
    private: std::string _str2;
};

std::forward<T> impl?

template <class T> 
constexpr T&& forward(typename remove_reference<T>::type& t) noexcept{
    return static_cast<T&&>(t);
}

template <class T> 
constexpr T&& forward(typename remove_reference<T>::type&& t) noexcept{
    return static_cast<T&&>(t);
}

How it Works?

  • LValues deduced as REFERENCES, but RValues are non-references
template <typename T>
void Foo (T&& arg){}    // For LValues, T is T&
                        // For RValues, T is T

Shmoo i;
Foo(i); // "T" above is int& 
        // (function takes reference to a reference of Shmoo, 
        // folds to a single reference to Shmoo)
Foo(std::move(i)); // "T" above is just "Shmoo" (function takes an RValue reference
                   // to Shmoo)

How it Works?

  • Uses "Reference Collapsing":
    • Reference-to-a-reference is illegal, so the compiler removes '&' in pairs
template <class T> T&& forward(typename remove_reference_t<T>& t) {return static_cast<T&&>(t);}
template <class T> T&& forward(typename remove_reference_t<T>&& t) {return static_cast<T&&>(t);}
// Passing an LValue, "T" is "T&", so ends up being:
T&&& forward(typename remove_reference_t<T&>& t){return static_cast<T&&&>(t);}
// Collapsed to:
T& forward(typename remove_reference_t<T>& t){return static_cast<T&>(t);}

// Passing an Value, "T" is still "T", so ends up being (2nd Overload):
T&& forward(typename remove_reference_t<T>&& t){return static_cast<T&&>(t);}

Q&A

Erich.Keane@intel.com

Move Semantics

By Erich Keane

Move Semantics

  • 770