Skip to content
C++
Language
since C++98
Intermediate

Special Member Functions

C++ special member function generation, suppression, and deletion rules — Rule of Zero, Rule of Five, =default, =delete, and every generation condition.

Special Member Functionssince C++98

Six class member functions — default constructor, destructor, copy constructor, copy assignment operator, move constructor (C++11), and move assignment operator (C++11) — that the compiler can generate automatically, subject to suppression rules determined by which members the user declares.

Overview

C++ has six special member functions. Four existed since C++98; C++11 added move semantics and two more:

cpp
class T {
    T();                              // 1. default constructor    (C++98)
    ~T();                             // 2. destructor             (C++98)
    T(const T&);                      // 3. copy constructor       (C++98)
    T& operator=(const T&);           // 4. copy assignment        (C++98)
    T(T&&) noexcept;                  // 5. move constructor       (C++11)
    T& operator=(T&&) noexcept;       // 6. move assignment        (C++11)
};

Two distinct outcomes arise when a special member is suppressed:

  • Not declared — the function simply does not exist. The call fails with "no matching function," and overload resolution may silently fall back to a less-efficient alternative (e.g., copying instead of moving).
  • Defined as deleted — the function exists but is explicitly = delete. Any call produces a hard "use of deleted function" diagnostic. No silent fallback.

Understanding which outcome applies is critical for diagnosing unexpected behavior.

Generation Rules

Default Constructor

Generated if the user declares no constructor of any kind — including copy and move constructors.

cpp
struct A { int x; };             // compiler generates A()
struct B { B(int); };            // user ctor → A() not generated
struct C { C() = default; };     // explicitly re-enable after suppression

Destructor

Always generated unless user-declared. Since C++11, the generated destructor is implicitly noexcept(true) and invokes member and base destructors in reverse declaration order.

Copy Constructor and Copy Assignment (C++98)

Generated by default. Since C++11:

  • Suppressed (not declared) when the user declares a move constructor or move assignment operator.
  • Defined as deleted when any non-static member or base has an inaccessible or deleted corresponding copy operation.

Deprecation (C++11): When the user declares a destructor, the copy constructor and copy assignment operator are still generated — but this behavior is deprecated. Compilers may warn. Prefer = default explicitly when you mean to keep them.

Move Constructor and Move Assignment (C++11)

Generated only when none of the following are user-declared: copy constructor, copy assignment operator, the complementary move operation, or destructor.

cpp
struct NoMove {
    ~NoMove() {}    // user destructor → move ctor and move assign NOT generated
};
NoMove a;
NoMove b = std::move(a);  // silently falls back to copy constructor — no diagnostic

The compiler-generated move operations are noexcept unless a member's or base's corresponding operation is not noexcept. This matters: std::vector only uses moves on reallocation when the move constructor is noexcept; otherwise it copies, silently degrading performance.

Suppression Table

User declaresDefault ctorDtorCopy ctorCopy assignMove ctorMove assign
(nothing)GenGenGenGenGenGen
Default constructorUserGenGenGenGenGen
DestructorGenUserGen†Gen†
Copy constructorGenUserGen
Copy assignmentGenGenGenUser
Move constructorGenDelDelUser
Move assignmentGenGenDelDelUser

= not generated (not declared; using it is a compile error — no silent fallback)
Del = defined as deleted (= delete; hard error with "use of deleted function")
† = generated but deprecated (C++11); declare explicitly or use = default

Rule of Zero

If your class members manage their own lifecycle — std::unique_ptr, std::shared_ptr, std::vector, std::string — declare none of the six special members. The compiler-generated versions compose correctly:

cpp
class Connection {
    std::string host_;
    std::uint16_t port_;
    std::unique_ptr<Socket> socket_;    // move-only: suppresses copy on Connection
    std::shared_ptr<Logger> logger_;    // reference-counted: copyable

    // No destructor, no copy/move declared.
    // Compiler generates:
    //   destructor:        calls member dtors in order
    //   move ctor/assign:  moves all members
    //   copy ctor/assign:  DELETED — unique_ptr copy is deleted
    //                      → Connection is implicitly move-only; correct
};

Connection a{...};
Connection b = std::move(a);  // ok: moves socket_ and logger_
Connection c = a;             // error: use of deleted function — clear intent

The Rule of Zero is self-documenting: ownership semantics fall directly out of member types. No custom logic, no maintenance burden.

Rule of Five

When you manage a raw resource — raw pointer, file descriptor, POSIX handle — the destructor alone suppresses moves and deprecates copies. Define all five (plus the destructor):

cpp
class Buffer {
    std::byte* data_;        // std::byte since C++17; use char* for C++11/14
    std::size_t size_;

public:
    explicit Buffer(std::size_t n)
        : data_{new std::byte[n]}, size_{n} {}

    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& o)                                          // copy ctor
        : data_{new std::byte[o.size_]}, size_{o.size_}
    { std::copy_n(o.data_, size_, data_); }

    Buffer& operator=(const Buffer& o) {                             // copy assign
        Buffer tmp{o};          // strong exception guarantee: copy first
        swap(*this, tmp);       // then swap (noexcept)
        return *this;
    }

    Buffer(Buffer&& o) noexcept                                      // move ctor
        : data_{std::exchange(o.data_, nullptr)}   // std::exchange since C++14
        , size_{std::exchange(o.size_, 0)} {}

    Buffer& operator=(Buffer&& o) noexcept {                         // move assign
        Buffer tmp{std::move(o)};
        swap(*this, tmp);
        return *this;
    }

    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
};

The copy-and-swap idiom (via a named swap friend) provides the strong exception guarantee for copy assignment and eliminates explicit self-assignment checks. Move assignment delegates to the move constructor and swap, keeping logic in one place.

= default and = delete

= default asks the compiler to generate a function under its normal rules, even after suppression:

cpp
// Base class with virtual destructor — moves are suppressed
class Base {
public:
    virtual ~Base() = default;          // user-declared, but still generated
    Base(Base&&) = default;             // explicitly re-enable
    Base& operator=(Base&&) = default;
protected:
    Base() = default;
    Base(const Base&) = default;
    Base& operator=(const Base&) = default;
};

= default outside the class definition generates the function in that translation unit — essential for PIMPL, where the destructor must be generated after Impl is complete:

cpp
// widget.h
class Widget {
public:
    Widget();
    ~Widget();                          // declared but not defined here
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

// widget.cpp
#include "widget.h"
#include "impl.h"    // Impl is now complete

Widget::~Widget() = default;            // C++11: compiler generates here, after Impl is visible
Widget::Widget(Widget&&) noexcept = default;
Widget::Widget& Widget::operator=(Widget&&) noexcept = default;

= delete makes any function uncallable — including non-member functions and template instantiations:

cpp
// Prevent implicit conversion from double to int in a numeric API
void process(int) {}
void process(double) = delete;   // process(3.14) is now a hard error, not a silent truncation

// Disable specific template instantiations (C++11)
template<typename T>
void serialize(T*);

template<>
void serialize(void*) = delete;  // void* overload disabled

Best Practices

  • Default to Rule of Zero. Write classes whose members own resources through smart pointers or standard containers. Zero special members = zero maintenance.
  • If you write a destructor, explicitly declare or = default all five others. The deprecated implicit copies and suppressed moves are latent bugs: they appear to work until someone adds a move operation.
  • Always mark generated move operations noexcept. Without it, std::vector reallocations copy instead of move. Check with static_assert(std::is_nothrow_move_constructible_v<T>).
  • Use = default on virtual destructors in base classes. A = default destructor is still user-declared (suppressing moves), but you can restore moves explicitly with = default on each operation.
  • Prefer copy-and-swap for copy assignment when strong exception safety matters. The pattern unifies copy and move assignment when using the unified value-parameter form, though note: the value-parameter version is not noexcept because the copy construction of the parameter may throw.

Common Pitfalls

Missing noexcept on move operations silently degrades std::vector performance:

cpp
class Tracker {
public:
    Tracker(Tracker&&) {}              // no noexcept specifier
    Tracker& operator=(Tracker&&) {}
};
static_assert(std::is_nothrow_move_constructible_v<Tracker>);  // fails — C++11
// std::vector<Tracker> reallocates by copying, not moving

Relying on deprecated copy generation with a user-declared destructor:

cpp
struct Resource {
    FILE* fp_;
    ~Resource() { if (fp_) fclose(fp_); }
    // Copy ctor and copy assign are generated — but deprecated.
    // Later, someone adds:
    //   Resource(Resource&&) = default;
    // This DELETES the copies (Del in the table).
    // Code that was compiling silently now fails.
};

"Not declared" vs. "deleted" produces different fallback behavior:

cpp
struct A {
    A(A&&) = default;   // declaring move → copy ctor is DELETED (Del)
};
A x;
A y = x;  // error: use of deleted function A::A(const A&)
           // no fallback attempted; hard error

struct B {
    std::unique_ptr<int> p_;  // copy ctor not declared (—)
};
B a;
B b = a;  // error: use of deleted function B::B(const B&)
           // (deleted transitively through unique_ptr)

Self-assignment in manual copy assignment without protection:

cpp
// Broken — naive implementation
Buffer& operator=(const Buffer& o) {
    delete[] data_;                      // if o == *this, o.data_ is now freed!
    data_ = new std::byte[o.size_];      // UB: reads o.size_ from freed memory
    std::copy_n(o.data_, o.size_, data_);
    return *this;
}
// Fix: allocate first, then delete; or use copy-and-swap

See Also

  • Move Semantics — value categories, std::move, and std::forward
  • RAII — the pattern that makes Rule of Zero work
  • PIMPL Idiom= default destructor placement in .cpp
  • Type Erasure — virtual destructor + Rule of Five in polymorphic wrappers