Move Semantics and Smart Pointers in Modern C++
#include <memory>
#include <vector>
class Resource {
int* data;
public:
Resource() : data(new int[100]) {}
~Resource() { delete[] data; }
// Move constructor
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr; // Prevent double deletion
}
// Move assignment
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
// Disable copying
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
};
void process_resource(std::unique_ptr<Resource> res) {
// Exclusive ownership transferred
}
int main() {
auto ptr = std::make_unique<Resource>(); // Smart pointer
std::vector<Resource> resources;
resources.push_back(Resource()); // Move semantics in action
process_resource(std::move(ptr)); // Explicit ownership transfer
}
Move Semantics: Beyond Copying
The Problem Move Solves
Traditional C++ copies objects unnecessarily:
std::vector<std::string> createStrings() {
std::vector<std::string> vec{"large", "data"};
return vec; // Pre-C++11: Expensive copy
} // Post-C++11: Efficient move
Key Components
- Rvalue References (
&&
)void foo(std::string&& s); // Accepts temporary objects
- Move Constructor
MyClass(MyClass&& other) noexcept;
- Move Assignment Operator
MyClass& operator=(MyClass&& other) noexcept;
The std::move
Utility
- Doesn't actually move anything
- Casts an lvalue to rvalue reference
- Signals "this object can be moved from"
std::string s1 = "Hello";
std::string s2 = std::move(s1); // s1 now in valid but unspecified state
Smart Pointers: Automatic Memory Management
Comparison of Smart Pointers
Type | Ownership | Thread-Safe | Use Case |
---|---|---|---|
std::unique_ptr | Unique | No | Exclusive ownership |
std::shared_ptr | Shared | Yes (ref count) | Shared ownership |
std::weak_ptr | Weak | Yes | Break reference cycles |
std::auto_ptr | Unique | No | Deprecated (use unique_ptr) |
std::unique_ptr
auto ptr = std::make_unique<MyClass>(); // Preferred creation
ptr->doSomething(); // Normal pointer syntax
// Ownership transfer
std::unique_ptr<MyClass> newOwner = std::move(ptr);
std::shared_ptr
auto shared = std::make_shared<MyClass>();
auto shared2 = shared; // Reference count increases
// With custom deleter
std::shared_ptr<FILE> file(
fopen("data.txt", "r"),
[](FILE* f) { if(f) fclose(f); }
);
std::weak_ptr
auto shared = std::make_shared<MyClass>();
std::weak_ptr<MyClass> weak = shared;
if (auto temp = weak.lock()) { // Convert to shared_ptr
temp->use(); // Safe to use
}
Rule of Five (Since C++11)
For resource-managing classes, define or delete:
- Destructor
- Copy constructor
- Copy assignment
- Move constructor
- Move assignment
class ManagedResource {
public:
// Constructor/destructor
ManagedResource();
~ManagedResource();
// Copy operations (deleted)
ManagedResource(const ManagedResource&) = delete;
ManagedResource& operator=(const ManagedResource&) = delete;
// Move operations
ManagedResource(ManagedResource&&) noexcept;
ManagedResource& operator=(ManagedResource&&) noexcept;
};
Performance Impact
Where Move Semantics Help
- Return value optimization (RVO)
std::vector<int> createVector() { return std::vector<int>{1,2,3}; // No copy/move happens (RVO) }
- Container operations
std::vector<std::string> vec; vec.push_back(std::string("test")); // Moves instead of copies
- Algorithm efficiency
std::sort(vec.begin(), vec.end()); // Swaps elements via moves
Common Pitfalls
-
Moving from objects still needed
auto s1 = std::string("text"); auto s2 = std::move(s1); // s1 is now empty - accessing it is legal but often unintended
-
Not marking moves as
noexcept
- Many standard library operations require noexcept moves
-
Mixing smart pointers with raw pointers
MyClass* raw = shared_ptr.get(); delete raw; // Disaster - double deletion
-
Circular references with
shared_ptr
struct Node { std::shared_ptr<Node> next; // Use weak_ptr instead for cycles };
Best Practices
-
Prefer
make_shared
/make_unique
- Exception safety
- Single allocation (for shared_ptr)
-
Use
const&
for "in" parameters- For non-movable, non-copyable types
- When you don't need to store the parameter
-
Return by value
- Let compiler optimize with RVO/move
-
Mark move operations as
noexcept
- Enables optimizations in standard containers
-
Follow RAII principles
- Acquire resources in constructors
- Release in destructors
Advanced Topics
Perfect Forwarding
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // Preserves value category
}
Custom Deleters
std::unique_ptr<FILE, decltype(&fclose)>
file(fopen("data.txt", "r"), &fclose);
Type Erasure with std::shared_ptr
std::shared_ptr<void> eraseType =
std::make_shared<std::string>("hidden");
Further Reading
- Effective Modern C++ by Scott Meyers (opens in a new tab)
- C++ Core Guidelines (opens in a new tab)
- Move Semantics in Depth (cppreference) (opens in a new tab)
- Smart Pointer Best Practices (opens in a new tab)
Last updated on April 10, 2025