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
= defaultasks for the compiler's generated version explicitly.- It is not the same as
{}—= defaultkeeps the type trivial,{}does not. = deleteremoves 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.
= deleteon an overload bans an unwanted implicit conversion.- An empty destructor is the mistake; write
= defaultor write nothing.
Questions about this lesson
Log in to ask a question.