C++实践中移动语义的适用场景及自定义移动函数的时机
Great question! Your observation about GCC and copy elision is spot-on—copy elision (especially mandatory RVO in C++17) can make it easy to miss when move semantics actually kicks in. Let's break this down into clear, practical parts:
When Move Semantics Actually Kick In
Even with aggressive copy elision (and mandatory RVO in C++17 for specific cases), there are plenty of scenarios where move operations are still called:
Explicitly converting lvalues to rvalues with
std::move(): This is the most direct case. When you usestd::move()to cast an lvalue to an rvalue reference, the compiler will trigger move operations instead of copies. For example:std::string large_str = "a very long string with lots of data"; std::string moved_str = std::move(large_str);Copy elision doesn't apply here because we're explicitly transferring ownership of
large_str's resources tomoved_str.Container operations: Containers rely heavily on move semantics for efficiency. For example:
- Adding rvalues (temporaries or moved objects) with
push_back()oremplace_back()will trigger moves instead of copies. - When a container like
std::vectorresizes, it will move existing elements to the new memory block (if the element type supports moving) instead of copying them.
std::vector<std::unique_ptr<int>> ptr_list; ptr_list.push_back(std::make_unique<int>(42)); // Moves the temporary unique_ptr std::unique_ptr<int> my_ptr = std::make_unique<int>(100); ptr_list.push_back(std::move(my_ptr)); // Transfers ownership of my_ptr to the vectorTypes like
std::unique_ptrcan't be copied at all, so move semantics are mandatory here.- Adding rvalues (temporaries or moved objects) with
Cases where copy elision isn't allowed:
- In C++17 and earlier, returning a conditional expression (e.g.,
return condition ? local_obj1 : local_obj2;) can't use copy elision—instead, the compiler will move the result. - If you return a value that's not a local variable or temporary (like a function's return value), copy elision may not apply, and a move will be used.
- When you explicitly disable copy elision with the
-fno-elide-constructorsflag, as you did in your tests—this forces the compiler to use move operations instead of eliding copies entirely.
- In C++17 and earlier, returning a conditional expression (e.g.,
Functions accepting rvalue references: When you pass an rvalue (temporary or moved lvalue) to a function that takes an rvalue reference parameter, move operations are triggered. For example:
void process_large_data(std::vector<int>&& data) { // Operate on the transferred data } process_large_data(std::vector<int>{1, 2, 3, 4, 5}); // Moves the temporary vector
When to Implement Custom Move Functions
The compiler generates default move constructors and move assignment operators automatically—but only if you don't declare any custom copy constructor, copy assignment operator, or destructor (this rule was adjusted slightly in C++17, but the core logic holds). You need to write custom move functions in these scenarios:
Your class manages raw resources: If your class owns dynamic memory (e.g., a
char*buffer), file handles, network sockets, or other resources requiring manual cleanup, the default move functions will only do a shallow copy. This leads to double-free errors or dangling pointers. For example:class CustomBuffer { private: char* buf; size_t size; public: // Custom move constructor CustomBuffer(CustomBuffer&& other) noexcept : buf(other.buf), size(other.size) { other.buf = nullptr; // Transfer ownership, leave original in valid state other.size = 0; } // Custom move assignment CustomBuffer& operator=(CustomBuffer&& other) noexcept { if (this != &other) { delete[] buf; // Clean up our own resource first buf = other.buf; size = other.size; other.buf = nullptr; other.size = 0; } return *this; } // Destructor to clean up the buffer ~CustomBuffer() { delete[] buf; } };Here, we explicitly transfer the buffer pointer and null out the original object to avoid double deletion.
You need custom logic during the move: If moving an object requires updating external state (e.g., notifying a resource manager that ownership has changed) or handling non-resource-related state transitions, the default move functions won't cover this. Custom moves let you include this necessary logic.
Your class has non-movable members: If some members of your class don't support move semantics (like legacy types without move functions), but you still want to implement a valid move operation for the class, you'll need to manually handle those members (e.g., copy them while moving others) in your custom move functions.
A quick rule of thumb: If your class only uses standard library types (like std::string, std::vector) that already support move semantics, and you don't have custom copy/destructor functions, the compiler-generated move functions will work perfectly—no need to write your own.
内容的提问来源于stack exchange,提问作者morte

