When Move Semantics Changes Observable Behavior

Move semantics in C++ were introduced primarily as a performance optimization. By allowing resources to be transferred rather than copied, they can significantly reduce overhead in many situations. However, there’s a subtle aspect that’s easy to miss: move operations can sometimes change the observable behavior of your program.

Consider this example:

#include <iostream>
#include <vector>
#include <string>

class Resource {
private:
    std::string name;
    std::vector<int> data;
    
public:
    Resource(std::string n, std::vector<int> d) 
        : name(std::move(n)), data(std::move(d)) {}
    
    // Print the first element, if any
    void use() const {
        std::cout << "Resource " << name << ": ";
        if (!data.empty()) {
            std::cout << "First element is " << data[0];
        } else {
            std::cout << "No elements";
        }
        std::cout << std::endl;
    }
};

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    Resource r1("Original", numbers);
    Resource r2("Copy", numbers);
    Resource r3("Move", std::move(numbers));
    
    r1.use();  // Shows data
    r2.use();  // Shows data
    r3.use();  // Shows data
    
    // After the move, the source is in a valid but unspecified state
    if (numbers.empty()) {
        std::cout << "Numbers is now empty after move" << std::endl;
    } else {
        std::cout << "Numbers still has " << numbers.size() << " elements" << std::endl;
    }
    
    return 0;
}

Most programmers focus on the performance benefits of std::move, but there’s a functional difference too. After moving from numbers into r3, the numbers vector is left in a valid but unspecified state. The standard requires it to be usable (i.e., you can call any method on it), but doesn’t guarantee what data it contains.

In practice, with most standard library implementations, the vector will be empty after the move. This means a moved-from object might behave differently in subsequent code, and this isn’t just an implementation detail – it’s a behavioral change your code might depend on.

This characteristic is particularly important when refactoring. If you change a copy operation to a move operation, you not only change performance characteristics but may also change how the source object behaves afterward. Any code that assumes the source is unchanged will now be incorrect.

Remember these guidelines:

  1. Don’t use moved-from objects except to assign them new values or destroy them
  2. If you must use them, only rely on operations with explicitly defined behavior for moved-from objects
  3. Document clearly when an object might be moved from

Move semantics is a powerful feature, but it’s not just about making things faster – it changes the contract between different parts of your code.