What's Behind the Clever Term "Move Semantics"?
\n\nImagine you're moving to a new apartment. You have a huge, heavy sofa. What's faster: dragging it yourself down five flights of stairs, or simply handing over the keys to the movers and saying, "Guys, the sofa is yours, do whatever you want with it, I don't need it anymore"? The second option is essentially move semantics in C++.
\n\nBefore the C++11 standard, programmers painfully copied data. Every time you returned a vector or a string from a function, the compiler created an exact copy. If the vector contained a million elements, it honestly copied each one. This was slow, inefficient, and resembled rewriting a book by hand instead of just handing over the original.
\n\nMove semantics is a mechanism that allows you to move resources (memory, file descriptors, sockets) from one object to another without expensive copying. After the move, the source object remains in an "empty" but valid state, and it can be safely destroyed or assigned a new value.
\n\nIn this article, we'll break down rvalue references, the std::move function, move constructors, and move assignment operators — in simple terms with code examples.
\n\nWhy Does Code Slow Down Without Move Semantics?
\n\nLet's look at a classic example that plagued all C++ programmers before 2011:
\n\n#include <iostream>\n#include <vector>\n#include <string>\n\nstd::vector<std::string> createBigVector() {\n std::vector<std::string> vec(1000000, "Hello");\n return vec; // Horror! Copying a million strings!\n}\n\nint main() {\n std::vector<std::string> data = createBigVector();\n std::cout << "Size: " << data.size() << std::endl;\n return 0;\n}\n\nWhat's happening here? The function createBigVector creates a vector, fills it with data, and returns it. The compiler (without optimizations) does the following:
- \n
- Creates the vector inside the function. \n
- Copies the ENTIRE vector into the return value. \n
- Destroys the original vector. \n
- In
main, copies that value into the variabledata. \n
Result: two full copies of a million strings. This isn't just slow — it's a performance disaster.
\n\nRvalue and Lvalue: Two Worlds of C++
\n\nTo understand move semantics, you need to grasp two fundamental concepts: lvalue and rvalue.
\n\n- \n
- Lvalue (left value) — an object that has an address in memory and a name. You can take its address (
&obj). Example: the variableint x = 5;—xis an lvalue. \n - Rvalue (right value) — a temporary value that has no name and cannot appear on the left side of an assignment operator. Example:
5,x + 3, the result of a functiongetTemp(). You cannot write&(x + 3). \n
Previously, rvalue references (denoted by &&) did not exist. The compiler saw a temporary object and said, "Aha, this is an rvalue, the only thing I can do with it is copy it or destroy it." Move semantics gave the compiler a third option: steal resources from the rvalue, because it's going to die soon anyway.
How Does std::move and Rvalue References Work?
\n\nThe function std::move is simply a type cast. It doesn't physically move data; it just turns an lvalue into an rvalue, giving the compiler permission to "plunder" the object.
\n\nLet's look at a simple class that manages dynamic memory:
\n\n#include <iostream>\n#include <cstring>\n\nclass MyString {\nprivate:\n cha