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

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++11

A 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:

  • = default requests the compiler-generated definition. It is valid only on special member functions (and, since C++20, on comparison operators).
  • = delete removes 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

cpp
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_ptr to 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

cpp
#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

cpp
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);    // error

A 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:

cpp
template <typename T>
bool isLucky(T) = delete;         // C++11: catches everything not exactly int
bool isLucky(int n);              // non-template preferred for exact match

Deleting template specializations

Class members cannot be specialized in a different access level, but deleted specializations are fine at namespace scope:

cpp
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:

cpp
template <typename T>
    requires (!std::is_void_v<T>)                // C++20
void processPointer(T* p);

Polymorphic base classes

cpp
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

cpp
// 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

cpp
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 = default or = delete to make each decision explicit.
  • Use = delete instead of private-undefined. Diagnostics are earlier and clearer, and members and friends cannot slip through. Put deleted functions in the public section; some compilers report access errors before deletion, which obscures the message.
  • Use = default to restore a suppressed function, e.g. Widget() = default; after adding another constructor.
  • Add noexcept deliberately. 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;) so Foo() = default; yields a fully initialized object.
  • Delete rvalue overloads to catch dangling references: void use(const Config&&) = delete; (see how std::ref deletes 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 const member gets a deleted copy assignment. Check with std::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() = default vs Foo() {}. 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 new can 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.

See Also