C++ Move Constructor: rvalue Stealing & noexcept Best Practices

C++ Move Constructor: rvalue Stealing & noexcept Best Practices

이 글의 핵심

Practical guide to move constructors: stealing from rvalues, interaction with noexcept and containers, and common pitfalls.

What is a move constructor?

A constructor that transfers resources from an rvalue.

class Buffer {
    int* data;
    size_t size;
    
public:
    // Move constructor
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
};

Copy vs move

// Copy: duplicate resources
Buffer(const Buffer& other) {
    data = new int[other.size];
    std::copy(other.data, other.data + size, data);
}

// Move: transfer ownership
Buffer(Buffer&& other) noexcept {
    data = other.data;
    other.data = nullptr;
}

Practical examples

Example 1: Basic implementation

class String {
    char* data;
    size_t length;
    
public:
    String(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    ~String() {
        delete[] data;
    }
    
    // Copy constructor
    String(const String& other) : length(other.length) {
        data = new char[length + 1];
        strcpy(data, other.data);
        std::cout << "copy" << std::endl;
    }
    
    // Move constructor
    String(String&& other) noexcept 
        : data(other.data), length(other.length) {
        other.data = nullptr;
        other.length = 0;
        std::cout << "move" << std::endl;
    }
};

int main() {
    String s1("Hello");
    String s2 = s1;              // copy
    String s3 = std::move(s1);   // move
}

Example 2: Vector behavior

#include <vector>

class Widget {
public:
    Widget() { std::cout << "ctor" << std::endl; }
    Widget(const Widget&) { std::cout << "copy" << std::endl; }
    Widget(Widget&&) noexcept { std::cout << "move" << std::endl; }
};

int main() {
    std::vector<Widget> vec;
    vec.reserve(10);
    
    Widget w;
    vec.push_back(w);              // copy
    vec.push_back(std::move(w));   // move
    vec.emplace_back();            // construct in place
}

Example 3: Smart pointers

#include <memory>

class Resource {
public:
    Resource() { std::cout << "Resource ctor" << std::endl; }
    ~Resource() { std::cout << "Resource dtor" << std::endl; }
};

int main() {
    auto ptr1 = std::make_unique<Resource>();
    
    // Move only (no copy)
    auto ptr2 = std::move(ptr1);
    // ptr1 is nullptr
}

Example 4: Return value optimization

Buffer createBuffer(size_t size) {
    Buffer b(size);
    return b;  // move or RVO
}

int main() {
    Buffer b = createBuffer(100);
}

Why noexcept matters

// Without noexcept: vector may fall back to copy on reallocation
Buffer(Buffer&& other) {
    // ...
}

// With noexcept: vector can move elements
Buffer(Buffer&& other) noexcept {
    // ...
}

Common pitfalls

Pitfall 1: State after move

// Bad: moved-from not cleared
Buffer(Buffer&& other) noexcept {
    data = other.data;
    // other.data still non-null (risky)
}

// Good: null out source
Buffer(Buffer&& other) noexcept {
    data = other.data;
    other.data = nullptr;
}

Pitfall 2: Self-move

// Bad: no self-check
Buffer& operator=(Buffer&& other) noexcept {
    delete[] data;
    data = other.data;
    other.data = nullptr;
    return *this;
}

// Good: guard self-assignment
Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data;
        data = other.data;
        other.data = nullptr;
    }
    return *this;
}

Pitfall 3: Exception safety

// May throw
Buffer(Buffer&& other) {
    // ...
}

// noexcept when possible
Buffer(Buffer&& other) noexcept {
    // ...
}

Pitfall 4: std::move on return locals

// Bad: can block NRVO
Buffer func() {
    Buffer b(100);
    return std::move(b);
}

// Good: just return
Buffer func() {
    Buffer b(100);
    return b;
}

Types that move

// Move-capable
std::vector<int>
std::string
std::unique_ptr<int>

// No move (trivially copyable scalars)
int
double

FAQ

Q1: When is the move ctor used?

A: When constructing from an rvalue (including moved lvalues).

Q2: Is noexcept mandatory?

A:

  • Not required by the language
  • Important for std::vector and strong guarantees

Q3: Valid state after move?

A: Object must be valid but unspecified; nulling pointers is good practice.

Q4: What is std::move?

A: Cast to rvalue reference; does not move by itself.

Q5: Performance?

A: Pointer swaps instead of deep copies—big win for large objects.

Q6: Resources?

A:

  • Effective Modern C++
  • C++ Move Semantics (general references)
  • cppreference.com

  • noexcept specifier
  • Rule of Five
  • Exception performance

Practical tips

Debugging

  • Warnings first, minimal repro

Performance

  • Measure before tuning

Code review

  • Team conventions

Practical checklist

Before coding

  • Right tool for the job?
  • Maintainable?
  • Performance OK?

While coding

  • Warnings fixed?
  • Edge cases?
  • Errors handled?

At review

  • Clear intent?
  • Tests?
  • Docs?

Keywords

C++, move constructor, move semantics, C++11, performance


  • Algorithm sort
  • async & launch
  • Atomic operations
  • Attributes
  • auto keyword