Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 12 of 12

Two small pieces of syntax that let you state your intent about the special member functions instead of leaving it implied.

= default

= default asks the compiler for its generated version, explicitly:

#include <iostream>

class Widget {
    int value = 0;               // default member initialiser

public:
    Widget(int v) : value(v) {}

    // Writing a constructor removed the free default one (lesson 9.3).
    // This asks for it back.
    Widget() = default;

    int get() const { return value; }
};

int main() {
    Widget a(42);
    Widget b;                    // works again

    std::cout << a.get() << "\n";
    std::cout << b.get() << "\n";
    return 0;
}
42
0

Note the int value = 0;. Without it, Widget() = default; would leave value uninitialised — the generated default constructor does nothing at all for a plain int member, so b.get() would read garbage (lesson 3.1). = default asks for the compiler's version; it does not ask for a sensible one. Pair it with default member initialisers whenever you want a known state.

Why not just write Widget() {}?

They are not the same, and the difference is not cosmetic:

#include <iostream>
#include <type_traits>

struct Defaulted {
    int x;
    Defaulted() = default;
};

struct UserProvided {
    int x;
    UserProvided() {}
};

int main() {
    std::cout << "Defaulted is trivially default constructible:    "
              << std::is_trivially_default_constructible<Defaulted>::value << "\n";
    std::cout << "UserProvided is trivially default constructible: "
              << std::is_trivially_default_constructible<UserProvided>::value << "\n";
    return 0;
}
Defaulted is trivially default constructible:    1
UserProvided is trivially default constructible: 0

= default keeps the type trivial, which lets the compiler optimise it as raw bytes and allows it in contexts requiring trivial types. {} makes the constructor user-provided and throws that away.

The same applies to the destructor, and this is the concrete version of lesson 9.11's warning: ~Widget() = default; still suppresses the move operations, but ~Widget() {} does too — so if you do not need a destructor, write neither.

= delete

= delete removes a function entirely. Calling it is a compile error, not a link error:

#include <iostream>

// A type that must not be copied — a lock, a file handle, a unique resource.
class NonCopyable {
    int id;

public:
    NonCopyable(int i) : id(i) {}

    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;

    // But moving is still fine.
    NonCopyable(NonCopyable&& other) noexcept : id(other.id) { other.id = -1; }
    NonCopyable& operator=(NonCopyable&& other) noexcept {
        if (this != &other) {
            id = other.id;
            other.id = -1;
        }
        return *this;
    }

    int get() const { return id; }
};

int main() {
    NonCopyable a(1);

    // NonCopyable b = a;      error: use of deleted function
    //                         'NonCopyable::NonCopyable(const NonCopyable&)'

    NonCopyable b = std::move(a);       // moving is allowed
    std::cout << "b=" << b.get() << " a=" << a.get() << "\n";
    return 0;
}
b=1 a=-1

Before C++11 this was done by declaring the copy operations private and never defining them, which produced a confusing linker error. = delete gives a clear compile-time message at the call site instead.

= delete on ordinary functions

It works on any function, which is useful for banning an unwanted implicit conversion:

#include <iostream>

void take_int(int n) {
    std::cout << "int: " << n << "\n";
}

// Without this, take_int(3.7) silently truncates to 3.
void take_int(double) = delete;

int main() {
    take_int(42);

    // take_int(3.7);     error: use of deleted function 'void take_int(double)'
    take_int(static_cast<int>(3.7));    // explicit truncation is fine
    return 0;
}
int: 42
int: 3

The deleted overload participates in overload resolution and wins for double, then the call is rejected. The caller has to say what they meant.

Common patterns

A non-copyable, movable resource — the standard shape for wrappers:

class FileHandle {
public:
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&&) noexcept;
    FileHandle& operator=(FileHandle&&) noexcept;
};

std::unique_ptr is exactly this, and Module 10 uses it constantly.

A type that cannot be copied or moved — for objects whose identity matters, like a mutex:

class Mutex {
public:
    Mutex(const Mutex&) = delete;
    Mutex& operator=(const Mutex&) = delete;
    Mutex(Mutex&&) = delete;
    Mutex& operator=(Mutex&&) = delete;
};

A static-only utility class — delete the constructor so nobody instantiates it:

#include <iostream>

class MathUtils {
public:
    MathUtils() = delete;                  // no instances, ever

    static int square(int n) { return n * n; }
    static int cube(int n) { return n * n * n; }
};

int main() {
    // MathUtils m;      error: use of deleted function 'MathUtils::MathUtils()'

    std::cout << MathUtils::square(4) << " " << MathUtils::cube(3) << "\n";
    return 0;
}
16 27

In modern C++ a namespace of free functions is usually better than a class of statics, but you will meet this pattern in existing code.

Saying what you mean

Together these let a class declare its full intent, so a reader does not have to work out what the compiler generated:

#include <iostream>
#include <vector>

class Snapshot {
    std::vector<int> data;

public:
    Snapshot(std::vector<int> d) : data(std::move(d)) {}

    Snapshot(const Snapshot&) = default;             // copying is fine
    Snapshot& operator=(const Snapshot&) = default;
    Snapshot(Snapshot&&) noexcept = default;         // moving is fine
    Snapshot& operator=(Snapshot&&) noexcept = default;
    ~Snapshot() = default;

    std::size_t size() const { return data.size(); }
};

int main() {
    Snapshot a({1, 2, 3});
    Snapshot b = a;
    Snapshot c = std::move(a);

    std::cout << b.size() << " " << c.size() << " " << a.size() << "\n";
    return 0;
}
3 3 0

Writing all five as = default is the explicit form of the Rule of Zero. It is more typing than writing nothing, and some codebases prefer it because the intent is visible. Both are fine — writing an empty destructor instead of a defaulted one is the mistake.

Try it

Write a class Connection that cannot be copied but can be moved, holding an int handle. Print in the move operations. Then try to copy it and read the error.

Checkpoint

  • = default asks for the compiler's generated version explicitly.
  • It is not the same as {} — = default keeps the type trivial, {} does not.
  • = delete removes a function; using it is a compile error at the call site.
  • Delete the copy operations for a resource wrapper that should only be movable.
  • Delete both copy and move for a type whose identity must not change.
  • = delete on an overload bans an unwanted implicit conversion.
  • An empty destructor is the mistake; write = default or write nothing.

Questions about this lesson

Log in to ask a question.