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