Special Member Functions in C++

Metin Cakircali

2 min read

In C++, an object's lifecycle semantics are implemented by the so-called special member functions; i.e., construction, copy, move, and destruction. In certain cases, the compiler may implicitly define a default member function, or a user-defined (explicit) special member function may prevent the generation of compiler defined (default) function. Actually, it is a complicated relationship between explicit and implicit definitions. That's why the C++ Core Guidelines advice to define either none (rule of zero) or all (rule of five) of the special member functions.

💡
Both = delete and = default are considered as explicit definitions.
class Simple {
public:
  Simple(const Simple&)                = default;  // copy constructor
  Simple& operator=(const Simple&)     = default;  // copy assignment
  Simple(Simple&&) noexcept            = default;  // move constructor
  Simple& operator=(Simple&&) noexcept = default;  // move assignment
  ~Simple()                            = default;  // destructor
  // ...
};

implicit copy issue

Declaring class destructor results in implicit copy semantics to be defaulted, which prevents the implicit move operations. The following example demonstrates two different cases where Copy class with only destructor, and Move class with destructor and move. In the case for Copy class, the compiler will default to generate copy special functions. In order to enable move semantics, Move class declares an explicit move assignment operator.

struct Base {
    int value {0};
};

struct Copy: Base {
    ~Copy() { }  // implicit copy
};

struct Move: Base {
    ~Move() { }                         // implicit copy
    Move& operator=(Move&&) = default;  // user-defined move assignment
};

Copy cA, cB;
cB = std::move(cA); // cA is copied

Move mA, mB;
mB = std::move(mA); // mA is moved

Canonical Operators

The C++ language provides quite a lot of built-in operators. There is no restriction on what the overloaded operators do (or what type they return), but there are some common knowledge expectations. For example, the assignment operators are expected to return by reference in order to allow x = y = z statements.

Assignment Operators: operator=

The two main semantics are copy and move. One may consider copy-and-swap if makes sense. Both copy and move assignment operators should be guarded against self-assignment, and return by reference. The move assignment should also be noexcept, and leave the moved-from (source) object in a valid (unspecified) state.

move assignment

The recommendation is to use = default whenever possible. If the class has non-smart members, such as raw pointers, then one should define the move assignment. It is important to set noexcept, guard against self-assignment, and leave the moved-from object in a valid state. An example below shows such a case.

#include <memory>
#include <utility>

class Simple {
public:

    // ...

    Simple& operator=(Simple&& other) noexcept {
        // self-assignment guard
        if (this == &other) { return *this; }
        // cleanup resources
        delete[] indexes;
        // set nullptr as valid state
        indexes = std::exchange(other.indexes, nullptr);
        // unique_ptr is movable
        left  = std::move(other.left);
        right = std::move(other.right);
        // return by reference
        return *this;
    }

private:
    int* indexes {nullptr};

    std::unique_ptr<Simple> left;
    std::unique_ptr<Simple> right;
};

copy-and-swap idiom

This is useful in special cases. It takes other by value, swaps its resources, and releases old resources via destructor of other. See example below:

Simple& Simple::operator=(Simple other) noexcept {
  data_.swap(other.data_);
  index_.swap(other.index_);
  return *this;
}

Move Semantics

Most of the time, compilers copy values because they cannot tell if a value is no longer needed.

💡
Use std::move() for no longer needed objects.