Move Semantics: Avoiding Expensive Copies
Before C++11, returning or passing large objects by value meant expensive copies. Move semantics allow transferring resources from temporary objects instead of copying them.
class Buffer {
char* data_;
size_t size_;
public:
// Constructor
Buffer(size_t s) : data_(new char[s]), size_(s) {}
// Copy constructor (deep copy)
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// Move constructor (transfer ownership)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
~Buffer() { delete[] data_; }
};
Move Constructor and Move Assignment
Move operations should be noexcept when possible—this enables optimizations in standard containers:
class Widget {
std::string name_;
std::vector<int> data_;
public:
// Move constructor
Widget(Widget&& other) noexcept
: name_(std::move(other.name_)),
data_(std::move(other.data_)) {
}
// Move assignment
Widget& operator=(Widget&& other) noexcept {
if (this != &other) {
name_ = std::move(other.name_);
data_ = std::move(other.data_);
}
return *this;
}
};
Note: std::move on member variables is necessary because a named rvalue reference (like other) is itself an lvalue!
std::move: Unconditional Cast to Rvalue
std::move doesn’t move anything—it just casts its argument to an rvalue reference, enabling move semantics:
std::string s1 = "hello";
std::string s2 = std::move(s1); // s1's data moved to s2
// s1 is now in "valid but unspecified" state
After std::move: The object remains alive and destructible, but its value is unspecified. Only reassign or destroy it.
std::string s = "hello";
std::string t = std::move(s);
// s.clear(); // OK: Reset to known state
// s = "new"; // OK: Reassign
// auto len = s.size(); // DANGEROUS: Value unspecified
Move-Only Types
Some types cannot be copied, only moved: unique_ptr, thread, iostream, etc.
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // Error: cannot copy
std::unique_ptr<int> p2 = std::move(p1); // OK: move
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(10)); // Move into vector
Return Value Optimization (RVO) and NRVO
Compilers can elide copies/moves when returning by value:
Widget createWidget() {
return Widget{}; // RVO: No move/copy, constructed directly in caller
}
Widget createWidget2() {
Widget w;
// ...
return w; // NRVO: Named Return Value Optimization (maybe)
}
Don’t std::move return values—it inhibits RVO:
Widget createWidget() {
Widget w;
return std::move(w); // BAD: Prevents NRVO, forces move
}
Perfect Forwarding with std::forward
When writing wrapper functions, you want to forward arguments exactly as received—preserving lvalue/rvalue-ness. This requires forwarding references (a.k.a. universal references).
template<typename T>
void wrapper(T&& arg) { // Forwarding reference
target(std::forward<T>(arg)); // Perfect forwarding
}
int x = 10;
wrapper(x); // T = int&, arg forwarded as lvalue
wrapper(10); // T = int, arg forwarded as rvalue
wrapper(std::move(x)); // T = int, arg forwarded as rvalue
Forwarding Reference Rules
T&& is a forwarding reference only when:
Tis a template type parameter- Type deduction occurs
template<typename T>
void func(T&& x); // Forwarding reference
template<typename T>
void func(std::vector<T>&& x); // NOT forwarding reference (no deduction for &&)
void func(int&& x); // NOT forwarding reference (not a template)
Variadic Template Forwarding
Combining variadic templates with perfect forwarding:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
auto p = make_unique<std::pair<int, std::string>>(42, "hello");
When to Use Move vs Forward
- std::move: When you know you have an rvalue or want to treat something as rvalue
- std::forward: In templates, when you want to preserve the value category
template<typename T>
void process(T&& arg) {
// Use arg multiple times...
target(std::forward<T>(arg)); // Forward on last use
}
Move Semantics and Exception Safety
Move operations should be noexcept when possible. Standard containers use move only if noexcept:
class Widget {
public:
Widget(Widget&& other) noexcept { /* ... */ }
Widget& operator=(Widget&& other) noexcept { /* ... */ }
};
std::vector<Widget> vec;
vec.push_back(Widget{}); // Uses move (noexcept)
// If move is not noexcept, vector uses copy (for strong exception guarantee)
What does std::move actually do?
Interactive Lab
Waiting for signal...