Default and Delete Functions
How = default and = delete control special member generation, overload resolution, and template instantiation in C++11 and later, with practical patterns.
Defaulted and deleted functionssince C++11A defaulted function (= default) asks the compiler to generate the standard implementation of a special member function, while a deleted function (= delete) makes any use of that function a compile-time error that still participates in overload resolution.
Overview
Before C++11, you controlled special members by either letting the compiler generate them implicitly or by declaring them private and leaving them undefined (the C++98 noncopyable idiom). Both approaches were awkward: the first gave no way to state intent, and the second produced linker errors, or access errors that members and friends could bypass.
C++11 introduced two function-body replacements:
= defaultrequests the compiler-generated definition. It is valid only on special member functions (and, since C++20, on comparison operators).= deleteremoves a function. It is valid on any function: members, free functions, and function template specializations.
A deleted function is still declared, so it takes part in overload resolution. If it is selected, the program is ill-formed at the call site, with a clear diagnostic, before linking.
Syntax
struct Widget {
Widget() = default; // C++11
Widget(const Widget&) = delete; // C++11
Widget& operator=(const Widget&) = delete; // C++11
Widget(Widget&&) noexcept = default; // C++11
Widget& operator=(Widget&&) noexcept = default;
~Widget() = default;
};
void log(int);
void log(double) = delete; // C++11: free function
struct Money {
long cents;
friend auto operator<=>(const Money&, const Money&) = default; // C++20
};Where = default appears matters:
- In-class (first declaration): the function is not user-provided. This keeps the type trivial where possible, and lets the class remain an aggregate in C++11β17 only if it has no user-declared constructors; since C++20 any user-declared constructor disqualifies aggregates.
- Out-of-class (
Widget::Widget() = default;in a.cpp): the function is user-provided, so it is never trivial. This is useful for hiding a definition (e.g.unique_ptrto an incomplete type, the pimpl idiom).
A deleted function must be deleted on its first declaration; deleting it later is ill-formed.
Examples
Non-copyable, movable resource handle
#include <utility>
#include <unistd.h>
class FileDescriptor {
public:
explicit FileDescriptor(int fd = -1) noexcept : fd_{fd} {}
~FileDescriptor() { if (fd_ >= 0) ::close(fd_); }
FileDescriptor(const FileDescriptor&) = delete; // C++11
FileDescriptor& operator=(const FileDescriptor&) = delete; // C++11
FileDescriptor(FileDescriptor&& o) noexcept : fd_{std::exchange(o.fd_, -1)} {}
FileDescriptor& operator=(FileDescriptor&& o) noexcept {
if (this != &o) { reset(); fd_ = std::exchange(o.fd_, -1); }
return *this;
}
void reset() noexcept { if (fd_ >= 0) ::close(fd_); fd_ = -1; }
private:
int fd_;
};Declaring the move operations suppresses the implicit copy operations anyway, but explicit deletion documents intent and improves diagnostics.
Rejecting unwanted implicit conversions
bool isLucky(int n); // the intended overload
bool isLucky(char) = delete; // C++11
bool isLucky(bool) = delete;
bool isLucky(double) = delete; // also catches float via promotion
isLucky(7); // OK
isLucky('a'); // error: use of deleted function
isLucky(3.5); // errorA deleted overload is a better match than the implicit conversion to int, so it wins and then fails. A single constrained template does the same job more compactly:
template <typename T>
bool isLucky(T) = delete; // C++11: catches everything not exactly int
bool isLucky(int n); // non-template preferred for exact matchDeleting template specializations
Class members cannot be specialized in a different access level, but deleted specializations are fine at namespace scope:
template <typename T>
void processPointer(T* p);
template <> void processPointer<void>(void*) = delete; // C++11
template <> void processPointer<char>(char*) = delete;
template <> void processPointer<const void>(const void*) = delete;
template <> void processPointer<const char>(const char*) = delete;Remember the const variants. For member templates, you cannot give the specialization different access, so delete it at namespace scope the same way, or use static_assert/constraints (C++20) instead:
template <typename T>
requires (!std::is_void_v<T>) // C++20
void processPointer(T* p);Polymorphic base classes
class Shape {
public:
Shape() = default;
virtual ~Shape() = default; // keep virtual dtor, trivially defined
virtual double area() const = 0;
protected:
Shape(const Shape&) = default; // allow derived copy, block slicing
Shape& operator=(const Shape&) = default;
};Making the copy operations protected prevents slicing through Shape values while letting derived classes implement cloning.
Class with a pimpl and out-of-line default
// widget.h
class Widget {
public:
Widget();
~Widget(); // declared only
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
private:
struct Impl;
std::unique_ptr<Impl> impl_; // C++11
};
// widget.cpp, Impl is complete here
Widget::Widget() = default;
Widget::~Widget() = default; // user-provided, defined where Impl is complete
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;Defaulted comparisons
struct Version {
int major, minor, patch;
auto operator<=>(const Version&) const = default; // C++20: also implies ==
bool operator==(const Version&) const = default; // C++20 (implicit if <=> is defaulted)
};Since C++20, a defaulted operator<=> generates <, <=, >, >= via rewriting, and also declares a defaulted operator==. Defaulted operator== alone is also available since C++20.
Best Practices
- Prefer the Rule of Zero. Use members that manage themselves (
std::vector,std::unique_ptr) and declare no special members at all. - If you declare one, declare all (Rule of Five). Use
= defaultor= deleteto make each decision explicit. - Use
= deleteinstead of private-undefined. Diagnostics are earlier and clearer, and members and friends cannot slip through. Put deleted functions in thepublicsection; some compilers report access errors before deletion, which obscures the message. - Use
= defaultto restore a suppressed function, e.g.Widget() = default;after adding another constructor. - Add
noexceptdeliberately. A defaulted function's exception specification is deduced from members. Since C++20, you may declare a mismatched explicit one; in C++11β17 a mismatch on the first declaration made the function deleted (C++11/14) or ill-formed, so verify against your target standard. - Default in-class members with initializers (
int x = 0;) soFoo() = default;yields a fully initialized object. - Delete rvalue overloads to catch dangling references:
void use(const Config&&) = delete;(see howstd::refdeletes rvalue overloads).
Common Pitfalls
- Defaulted becomes implicitly deleted. If a member or base is non-copyable, a defaulted copy constructor is silently defined as deleted. A class with a reference or
constmember gets a deleted copy assignment. Check withstd::is_copy_assignable_v. - Move suppresses copy. Declaring a move constructor or move assignment makes the implicit copy operations deleted. Declaring a copy operation or destructor does not delete the moves; it just stops them from being implicitly declared, so moves silently fall back to copies. Defaulted copy operations alongside a user-declared destructor are deprecated-but-generated.
Foo() = defaultvsFoo() {}. These are not equivalent. The defaulted version permits value-initialization to zero-initialize (Foo{}zeroes trivial members); the empty body leaves them indeterminate and makes the type non-trivial.- Deleted is not absent. Deleting a conversion or overload can make previously valid calls ambiguous or ill-formed, since it still participates in resolution. Deleting a constructor of a base also blocks derived classes that rely on it.
- Deleting only after the first declaration is ill-formed; so is defaulting a function with a signature that does not match a special member (e.g.
void f() = default;). - Deleted destructors make stack, static, or
delete-expression use ill-formed. They are legitimate for forcing heap-only or custom-lifetime objects, but are rarely needed. - Class-specific
operator newcan be deleted to forbid dynamic allocation:static void* operator new(std::size_t) = delete;. Placement forms remain unless also deleted. - Out-of-class defaulting is user-provided, which changes triviality,
std::is_trivially_copyable, and constexpr-ness (until C++20 relaxations); don't use it on types that must stay trivial. - Defaulted
<=>ordering follows declaration order of members. Reordering fields changes comparison results.