Fundamentals 20 min read

Understanding C++ Move Semantics and std::forward

The article explains C++11 move semantics, showing how std::move forces rvalue treatment and how std::forward preserves value categories for perfect forwarding, illustrating copy vs. move constructors, custom move implementations, and the performance benefits of eliminating unnecessary copies.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding C++ Move Semantics and std::forward

This article explains why C++11 introduced move semantics and how the standard library functions std::move and std::forward are used to avoid costly copy operations.

1. Copy semantics background

Before C++11, function arguments were always passed by copy. Copying large objects (including pointers) incurs heavy overhead. The article shows a simple example ( 0_copy_semantics.cc ) where an object is copied when passed to a function and when inserted into a std::vector . The program output demonstrates that three objects are created and three destructors are called.

#include
#include
class Object {
public:
    Object() { std::cout << "build this object!" << std::endl; }
    virtual ~Object() { std::cout << "destruct this object!" << std::endl; }
};

void f(const Object obj) {}

int main() {
    Object obj{};
    f(obj);
    std::vector
v;
    v.push_back(obj);
}

The output shows one construction and three destructions, indicating that copies were made during the function call and the vector insertion.

2. Rvalue (temporary) values

Copying temporary values is unnecessary. The article lists typical cases where a temporary is created (e.g., passing a string literal to a function, v.push_back(X()) , arithmetic expressions). It introduces the concept of Return Value Optimization (RVO/NRVO) that lets the compiler elide copies when returning objects.

3. Using std::move

A demonstration program ( 1_move_semantics.cc ) adds a std::string member to Object and prints the object's address. It shows the difference between a copy‑based function ( f_copy ) and a move‑based function ( f_move ).

#include
#include
#include
#include
class Object {
public:
    explicit Object(std::string str) : _str(std::move(str)) {
        std::cout << "build this object, address: " << this << std::endl;
    }
    virtual ~Object() { std::cout << "destruct this object, address: " << this << std::endl; }
    Object(const Object &object) : _str(object._str) { std::cout << "copy this object, address: " << this << std::endl; }
    Object(Object &&object) noexcept : _str(std::move(object._str)) { std::cout << "move this object!" << std::endl; }
    std::string _str;
};

void f_copy(Object obj) { std::cout << "copy function, address: " << &obj << std::endl; }
void f_move(Object &&obj) { Object a_obj(std::move(obj)); std::cout << "move function, address: " << &a_obj << std::endl; }

int main() {
    Object obj{"abc"};
    f_copy(obj);
    f_move(std::move(obj));
    std::cout << "============== end ===============" << std::endl;
    return 0;
}

The output confirms that f_copy invokes the copy constructor, while f_move invokes the move constructor and no copy occurs.

4. Implementing move semantics

A custom class MyString is presented to illustrate how to write a move constructor and move assignment operator that transfer ownership of a dynamically allocated buffer and set the source pointer to nullptr . The article also shows the consequences of forgetting to nullify the source (double free) or of using a moved‑from object (access violation).

#include
#include
#include
class MyString {
public:
    explicit MyString(const char *data) {
        if (data) { _data = new char[strlen(data) + 1]; strcpy(_data, data); }
        else { _data = new char[1]; *_data = '\0'; }
        std::cout << "built this object, address: " << this << std::endl;
    }
    ~MyString() { std::cout << "destruct this object, address: " << this << std::endl; delete[] _data; }
    MyString(const MyString &str) { std::cout << "copy this object, address: " << this << std::endl; _data = new char[strlen(str._data) + 1]; strcpy(_data, str._data); }
    MyString(MyString &&str) noexcept : _data(str._data) { std::cout << "move this object" << std::endl; str._data = nullptr; }
    MyString &operator=(const MyString &str) { if (this == &str) return *this; delete[] _data; _data = new char[strlen(str._data) + 1]; strcpy(_data, str._data); return *this; }
    MyString &operator=(MyString &&str) noexcept { if (this == &str) return *this; delete[] _data; _data = str._data; str._data = nullptr; return *this; }
    char *_data;
};

void f_move(MyString &&obj) {
    MyString a_obj(std::move(obj));
    std::cout << "move function, address: " << &a_obj << std::endl;
}

int main() {
    MyString obj{"abc"};
    f_move(std::move(obj));
    std::cout << "============== end ===============" << std::endl;
    return 0;
}

Running this program prints the construction, move, and destruction sequence, highlighting the importance of resetting the moved‑from object's internal pointer.

5. std::forward – perfect forwarding

When a template parameter is declared as T&& , the compiler can bind both lvalues and rvalues. std::forward preserves the original value category, allowing a single function to forward arguments without losing their lvalue/rvalue nature. The article provides a template example ( 2_forward.cc ) that forwards an Object either by copy or by move depending on the argument.

template<typename T>
void f_forward(T &&t) {
    Object a = std::forward<T>(t);
    std::cout << "forward this object, address: " << &a << std::endl;
}

int main() {
    Object obj{"abc"};
    f_forward(obj);               // lvalue → copy
    f_forward(Object("def"));    // rvalue → move
    return 0;
}

The output shows one copy construction and one move construction, confirming that std::forward correctly forwards the value category.

6. Differences between std::move and std::forward

std::move unconditionally casts its argument to an rvalue reference.

std::forward only casts to an rvalue when the original argument is an rvalue; otherwise it returns an lvalue reference.

Both are compile‑time type conversions; they generate no runtime code.

In practice, std::forward is used in generic/template code where the value category is unknown, while std::move is used when the programmer explicitly wants to treat a known object as a temporary.

7. Summary

The article concludes that both functions are essential tools for modern C++ programming. std::move forces a value to be treated as an rvalue, enabling move constructors/assignments. std::forward enables perfect forwarding in templates, preserving the original value category. Proper use of these utilities, together with correctly implemented move constructors and assignment operators, eliminates unnecessary copies and improves performance.

C++move semanticsC++11rvalue referencesstd::forward
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.