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-swapRule of Five in practice
The Rule of Five is a contract: if you need any one of the five resource-management SMFs, you almost certainly need all five. Managing a raw resource such as a heap allocation, a file descriptor, or a socket requires that every ownership transition — construction, copy, move, and destruction — be handled consistently. Omitting even one creates leaks, double-frees, or silent shallow copies.
// C++11 — Rule of Five applied to a fixed-size heap buffer
class RawBuffer {
int* data_;
size_t cap_;
public:
explicit RawBuffer(size_t n)
: data_{new int[n]}, cap_{n} {}
~RawBuffer() { delete[] data_; } // 1. destructor
RawBuffer(const RawBuffer& o) // 2. copy ctor
: data_{new int[o.cap_]}, cap_{o.cap_}
{ std::copy_n(o.data_, cap_, data_); }
RawBuffer& operator=(const RawBuffer& o) { // 3. copy assign
if (this != &o) {
int* tmp = new int[o.cap_];
std::copy_n(o.data_, o.cap_, tmp);
delete[] data_;
data_ = tmp;
cap_ = o.cap_;
}
return *this;
}
RawBuffer(RawBuffer&& o) noexcept // 4. move ctor
: data_{std::exchange(o.data_, nullptr)} // std::exchange: C++14
, cap_ {std::exchange(o.cap_, 0)} {}
RawBuffer& operator=(RawBuffer&& o) noexcept { // 5. move assign
if (this != &o) {
delete[] data_;
data_ = std::exchange(o.data_, nullptr);
cap_ = std::exchange(o.cap_, 0);
}
return *this;
}
};A common shortcut: implement move assignment in terms of the move constructor and a swap, as shown in the Rule of Five section above — this eliminates the duplicate exchange calls and keeps ownership logic in one place.
Rule of Zero: prefer no manual SMFs
The Rule of Zero is the modern default: compose your class from types that already own their resources and declare none of the six SMFs yourself. Smart pointers, containers, and standard wrappers carry the right semantics automatically, and the compiler synthesises correct, noexcept-where-possible SMFs from them.
// C++11 — Rule of Zero: no SMFs declared; correct semantics emerge from members
class Session {
std::string host_; // copyable + movable
std::unique_ptr<NetworkSocket> socket_; // move-only → Session is move-only
std::shared_ptr<spdlog::logger> logger_; // ref-counted → copyable per member rules
// Compiler generates:
// destructor: closes socket_, releases logger_ ref, destroys host_
// move ctor/assign: moves all three members
// copy ctor/assign: DELETED — unique_ptr copy is deleted
};If a class needs only a custom destructor for a side effect (logging, instrumentation) but no real resource ownership, wrap the side effect in a separate RAII guard and keep the owning class at Rule of Zero.
// C++11 — isolate the side effect; keep the owner clean
struct CloseLog {
~CloseLog() { log("session closed"); }
};
class Session {
CloseLog log_guard_; // destructor side effect here
std::unique_ptr<NetworkSocket> socket_; // Rule of Zero for the resource
};Deleted special members and type traits
When a special member is defined as deleted — either explicitly or transitively through a member — standard type traits let you detect this at compile time. This is useful in templates and static_assert guards to surface ownership mismatches early.
// C++11
#include <type_traits>
struct MoveOnly {
MoveOnly() = default;
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
MoveOnly(MoveOnly&&) noexcept = default;
MoveOnly& operator=(MoveOnly&&) noexcept = default;
};
static_assert(!std::is_copy_constructible_v<MoveOnly>, "should not be copyable"); // C++17 _v
static_assert( std::is_move_constructible_v<MoveOnly>, "should be movable");
static_assert( std::is_nothrow_move_constructible_v<MoveOnly>);The full set of relevant traits: is_default_constructible, is_copy_constructible, is_copy_assignable, is_move_constructible, is_move_assignable, is_destructible, and their is_nothrow_* and is_trivially_* variants. Prefer the _v helper variables (C++17) over ::value.
// C++20 — concepts built on type traits enforce SMF requirements on template parameters
template<typename T>
requires std::is_nothrow_move_constructible_v<T>
&& std::is_nothrow_move_assignable_v<T>
void swap_fast(T& a, T& b) noexcept {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}Conditionally noexcept special members
Compiler-generated SMFs inherit their noexcept specification from members and bases: if every sub-object's corresponding operation is noexcept, the generated SMF is noexcept too. For hand-written SMFs you must replicate this propagation manually using noexcept(noexcept(...)).
// C++11 — conditionally noexcept move constructor
template<typename T>
class Wrapper {
T val_;
public:
Wrapper(Wrapper&& o)
noexcept(std::is_nothrow_move_constructible_v<T>)
: val_{std::move(o.val_)} {}
Wrapper& operator=(Wrapper&& o)
noexcept(std::is_nothrow_move_assignable_v<T>)
{
val_ = std::move(o.val_);
return *this;
}
};For a class with multiple members, chain the conditions with &&:
// C++11
template<typename K, typename V>
struct Pair {
K key;
V val;
Pair(Pair&& o)
noexcept(std::is_nothrow_move_constructible_v<K>
&& std::is_nothrow_move_constructible_v<V>)
: key{std::move(o.key)}, val{std::move(o.val)} {}
};Getting noexcept wrong has silent performance consequences: std::vector and other containers call std::move_if_noexcept, which returns an lvalue reference (triggering a copy) whenever the move constructor is not noexcept.
SMFs and inheritance
A virtual destructor in a base class suppresses move generation and deprecates copies — so base classes almost always need the full set explicitly = default'd. Failing to do so leaves derived classes unable to move, even when all their own members support it.
// C++11 — canonical polymorphic base class
class Base {
public:
virtual ~Base() = default; // virtual dtor — user-declared → suppresses moves
Base(Base&&) noexcept = default; // re-enable moves
Base& operator=(Base&&) noexcept = default;
Base(const Base&) = default; // re-enable copies (deprecated without this)
Base& operator=(const Base&) = default;
protected:
Base() = default; // prevent direct instantiation while keeping aggregate init for derived
};Derived classes that add no raw resources can follow Rule of Zero; the generated SMFs call through to the base ones:
// C++11
class Derived : public Base {
std::string name_;
// No SMFs declared — compiler generates all six,
// each calling Base's corresponding = default SMF.
};When a base class has a deleted copy (e.g., it holds a std::mutex or unique_ptr), the derived class copy is also deleted transitively — this is correct and intentional. Use = delete explicitly on the base to make the intent clear rather than relying on transitive deletion.
Copy-and-swap idiom
Copy-and-swap provides the strong exception guarantee for copy assignment by building the replacement in a temporary before committing any change to *this. The idiom also eliminates explicit self-assignment checks and reuses the copy constructor.
// C++11
class Buffer {
std::byte* data_;
size_t size_;
public:
// ... (ctor, dtor, copy ctor, move ctor as before)
friend void swap(Buffer& a, Buffer& b) noexcept { // ADL-findable swap
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
// Unified copy+move assignment via value parameter (C++11)
Buffer& operator=(Buffer o) noexcept { // o is constructed by caller (copy or move)
swap(*this, o); // steal o's resources; o's dtor frees old ones
return *this;
}
};
Buffer a(64), b(128);
a = b; // copy: calls copy ctor for parameter o, then swap
a = std::move(b); // move: calls move ctor for parameter o (noexcept), then swapThe value-parameter form unifies copy and move assignment in a single function. Note: this form is not noexcept because the copy path may throw — declare separate copy and move assignment operators (as in the Rule of Five example above) when you need noexcept move assignment.
Move semantics and noexcept: why it matters for containers
std::vector — and the standard library broadly — moves elements during reallocation only when the move constructor is noexcept; otherwise it copies, silently turning O(1) moves into O(n) copies. This is governed by std::move_if_noexcept and checked at compile time.
// C++11 — demonstrating the copy-fallback penalty
#include <vector>
#include <type_traits>
class Expensive {
std::vector<double> data_;
public:
Expensive(Expensive&& o) /* no noexcept */ : data_{std::move(o.data_)} {}
// std::vector<Expensive> will COPY on reallocation — O(n) instead of O(1)
};
class Cheap {
std::vector<double> data_;
public:
Cheap(Cheap&& o) noexcept : data_{std::move(o.data_)} {}
// std::vector<Cheap> will MOVE on reallocation — O(1)
};
static_assert(!std::is_nothrow_move_constructible_v<Expensive>);
static_assert( std::is_nothrow_move_constructible_v<Cheap>);The same applies to std::deque, std::unordered_map, and other containers that relocate elements. Check every type you push into containers:
// C++11 — add this assertion alongside any type destined for containers
static_assert(std::is_nothrow_move_constructible_v<MyType>,
"MyType must be nothrow-movable to avoid copy fallback in std::vector");C++17 introduced std::launder and guaranteed copy elision, but neither removes the noexcept requirement for move_if_noexcept. C++20 concepts let you enforce this requirement in template interfaces directly (see Deleted special members and type traits above).
See 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