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++98Six 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:
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.
struct A { int x; }; // compiler generates A()
struct B { B(int); }; // user ctor → A() not generated
struct C { C() = default; }; // explicitly re-enable after suppressionDestructor
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
= defaultexplicitly 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.
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 diagnosticThe 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 declares | Default ctor | Dtor | Copy ctor | Copy assign | Move ctor | Move assign |
|---|---|---|---|---|---|---|
| (nothing) | Gen | Gen | Gen | Gen | Gen | Gen |
| Default constructor | User | Gen | Gen | Gen | Gen | Gen |
| Destructor | Gen | User | Gen† | Gen† | — | — |
| Copy constructor | — | Gen | User | Gen | — | — |
| Copy assignment | Gen | Gen | Gen | User | — | — |
| Move constructor | — | Gen | Del | Del | User | — |
| Move assignment | Gen | Gen | Del | Del | — | User |
— = 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:
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 intentThe 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):
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:
// 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:
// 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:
// 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 disabledBest 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
= defaultall 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::vectorreallocations copy instead of move. Check withstatic_assert(std::is_nothrow_move_constructible_v<T>). - Use
= defaulton virtual destructors in base classes. A= defaultdestructor is still user-declared (suppressing moves), but you can restore moves explicitly with= defaulton 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
noexceptbecause the copy construction of the parameter may throw.
Common Pitfalls
Missing noexcept on move operations silently degrades std::vector performance:
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 movingRelying on deprecated copy generation with a user-declared destructor:
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:
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:
// 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-swapSee Also
- Move Semantics — value categories,
std::move, andstd::forward - RAII — the pattern that makes Rule of Zero work
- PIMPL Idiom —
= defaultdestructor placement in .cpp - Type Erasure — virtual destructor + Rule of Five in polymorphic wrappers