C++11: = default, = delete C++20: defaulted comparisons
Every class carries six functions the compiler is willing to write for you — and a set of quiet rules deciding when it actually does. Before C++11, controlling that machinery meant tricks: empty bodies, private declarations left undefined. Modern C++ replaces the tricks with two direct statements. = default says generate the obvious implementation; = delete says this function does not exist, and saying so is a compile error.
The six special member functions
Default constructor, destructor, copy constructor, copy assignment, move constructor, move assignment. The generation rules that matter in practice:
- Declaring any constructor suppresses the implicit default constructor.
- Declaring a destructor or either copy operation suppresses the move operations. They aren't deleted — they simply never exist, so
std::movequietly falls back to copying. - Declaring either move operation deletes the copy operations. Move-aware types don't get accidental copies.
- The implicit copy operations are deprecated the moment you declare a destructor — recent compilers say so with
-Wdeprecated-copy-dtor.
The middle rule is the expensive one. A destructor added for logging is enough to turn every move of that type into a copy, with no diagnostic unless you ask for it.
= default: ask for the compiler's implementation
A defaulted function is the compiler-generated version, requested explicitly. That restores what a declaration above suppressed — and it is better than an empty body {}, because a defaulted function can remain trivial, while a user-provided one never is:
#include <print>
#include <utility>
struct loud {
loud() = default;
loud(const loud&) { std::println(" copy-construct"); }
loud(loud&&) { std::println(" move-construct"); }
};
struct dtor_only { // a destructor, and nothing else declared:
loud member; // the move operations are never generated
~dtor_only() {}
dtor_only() = default;
dtor_only(const dtor_only&) = default;
dtor_only& operator=(const dtor_only&) = default;
};
struct rule_of_five { // everything spelled out, nothing lost
loud member;
rule_of_five() = default;
~rule_of_five() = default;
rule_of_five(const rule_of_five&) = default;
rule_of_five& operator=(const rule_of_five&) = default;
rule_of_five(rule_of_five&&) = default;
rule_of_five& operator=(rule_of_five&&) = default;
};
int main() {
dtor_only a;
std::println("moving dtor_only:");
dtor_only b = std::move(a); // silently copies - there is no move constructor
rule_of_five c;
std::println("moving rule_of_five:");
rule_of_five d = std::move(c); // an actual move
(void)b; (void)d;
}
Two more things = default buys you:
- Triviality. A defaulted special member (on a type whose members allow it) keeps the class trivially copyable — eligible for
memcpy-style optimization and for passing in registers. An empty{}body forfeits that permanently. - Out-of-line defaulting.
widget::~widget() = default;in the .cpp file is the standard move for pimpl types: the header declares the destructor, and it's defaulted wherestd::unique_ptr's deleter can see the complete implementation type.
One C++20 caution: a type with any user-declared constructor — even = default — is no longer an aggregate, so brace-initialization of members stops working the aggregate way.
= delete: remove a function from the program
A deleted function still participates in overload resolution — it just makes the program ill-formed when chosen. That's the whole trick: you position deleted overloads exactly where dangerous arguments would land.
#include <print>
#include <string>
#include <utility>
// Accept real indices only. Floating-point callers get a compile error,
// not a silent truncation.
void store_index(int i) { std::println("stored {}", i); }
void store_index(double) = delete; // catches double AND float (promotion wins over conversion)
struct session {
std::string user;
explicit session(std::string u) : user(std::move(u)) {}
session(const session&) = delete; // identity type: two copies of a
session& operator=(const session&) = delete; // session is always a bug
session(session&&) = default; // transferring ownership is fine
session& operator=(session&&) = default;
};
void watch(const session& s) { std::println("watching {}", s.user); }
void watch(session&&) = delete; // refuse temporaries: we keep a reference
int main() {
store_index(42);
// store_index(3.14); // error: use of deleted function 'store_index(double)'
// store_index(2.5f); // error: float promotes to double - also deleted
session s{"gavin"};
watch(s);
// watch(session{"tmp"}); // error: a temporary would dangle behind our back
session t = std::move(s); // ownership transfer still allowed
std::println("moved to {}", t.user);
}
The patterns that come up again and again:
- Non-copyable types: delete the copy pair for sessions, mutexes, file handles — anything where identity matters. Delete all four and you have an immovable type (
std::mutexitself). - Blocking conversions: a deleted overload on the better-matching type intercepts arguments before they convert.
store_index(double) = deleterejects every floating-point call, because promotion todoubleoutranks conversion toint. - Refusing rvalues:
f(T&&) = deletenext tof(const T&)stops callers from passing temporaries to functions that retain a reference. - Any function can be deleted — free functions, member functions, even
operator newto keep a type off the heap. C++26 adds= delete("use store_index(int) instead")so the error message explains itself.
Before C++11, the closest tool was declaring the copy operations private and never defining them — a compile error for outsiders, a link error for members and friends, and a confusing message either way. = delete is diagnosed at the call site, immediately, with the reason visible in the declaration.
Defaulted comparisons (C++20)
The same = default syntax extends to comparison operators, which the compiler implements memberwise, in declaration order:
struct version {
int major, minor, patch;
auto operator<=>(const version&) const = default; // ==, !=, <, <=, >, >= all work
};
One line replaces six hand-rolled operators and the inevitable bug where someone compares minor before major. Comparisons get a full treatment in a later chapter; here it's enough to know the keyword composes.
Guidelines
- Start from the rule of zero: if a class manages no resource directly, declare none of the six and the compiler gets all of them right.
- The moment you declare one of destructor/copy/move, spell out all five —
= defaultfor the ones you want, so nothing silently disappears. - Prefer
= defaultto an empty body{}— it preserves triviality and documents intent. - Delete the copy operations of identity types, and use deleted overloads to reject conversions and dangling-prone temporaries at compile time.
- Turn on
-Wdeprecated-copy-dtor(it lives in-Wextra): it catches the "destructor killed my moves" trap the day it happens.