Constructors and Destructors Cheatsheet¶
Construction happens more often than you think — every temporary, every copy, every parameter. This sheet is about knowing exactly how many times, and in what order.
Pairs with Module 3 — Constructors and Destructors
Every form of constructor¶
| Form | Looks like | Callable with |
|---|---|---|
| Default | Account(); |
No arguments |
| Parameterised | Account(std::string owner, int opening); |
Two arguments |
| Delegating | Account() : Account("nobody", 0) {} |
Delegates to another |
| Copy | Account(const Account& other); |
Another Account (lvalue) |
| Move | Account(Account&& other) noexcept; |
An Account temporary |
| Converting | Account(int opening); — usually explicit |
One argument, possibly implicitly |
| Inherited | using Base::Base; in the derived class |
The base's constructors |
| Deleted | Account(const Account&) = delete; |
Never — a compile error to try |
Declaring any constructor suppresses the compiler's default constructor. If you want both,
write Account() = default;. This is the single most common "no matching function for call"
cause in first projects.
Member initialiser lists¶
Account::Account(std::string owner, int opening)
: owner_(std::move(owner)), // initialise, not assign
balance_(opening)
{
if (opening < 0) { throw std::invalid_argument("opening balance cannot be negative"); }
}
| Rule | Why it matters |
|---|---|
| Members are initialised in declaration order, not initialiser-list order | Reordering the list does not reorder construction; the compiler warns with -Wall (-Wreorder) if you list them out of order |
const members and references must be initialised here |
There is no other point at which they can receive a value |
| Initialise, do not assign in the body | Assignment runs after construction, so the member is constructed twice — which matters for anything non-trivial |
| Members are initialised before the body runs | So a body that throws means the object's members were already built and will be destroyed |
A throwing constructor means the object never existed. Its destructor does not run. Any resource already acquired in the member initialiser list is released, because those members were fully constructed — this is exactly why owning members should own via a value or a smart pointer rather than a raw pointer you free by hand.
Order: construction and destruction¶
Construction Destruction
1. base class subobject 1. derived class destructor body
2. member subobjects, declaration order 2. member subobjects, reverse declaration order
3. derived constructor body 3. base class destructor body
Destruction is the exact reverse of construction, always. Every leak investigation you do starts by writing this order down for the class in question.
struct Order
{
std::string name;
Order(std::string n) : name(std::move(n)) { std::cout << "+ " << name << '\n'; }
~Order() { std::cout << "- " << name << '\n'; }
};
Two members a then b print + a, + b, then - b, - a.
explicit, = default and = delete¶
explicit Account(int opening); // Account a = 500; → compile error (good)
// Account a(500); → fine
Account(const Account&) = delete; // copying is a compile error
Account& operator=(const Account&) = delete;
Account(Account&&) noexcept = default; // use the compiler's version, but say so
| Use | When |
|---|---|
explicit |
Any one-argument constructor, unless you want implicit conversion |
= default |
To keep the compiler's version while documenting that you thought about it |
= delete |
When a sensible meaning does not exist — a copyable bank account, a copyable turnstile |
Deleting is better than documenting: a rule the compiler enforces survives a tired afternoon.
Destructors: five rules¶
- A destructor must not let an exception escape. Since C++11 destructors are
noexceptby default, so athrowinside one callsstd::terminate— and the compiler says so:
error: 'throw' will always call 'terminate' [-Werror=terminate]
note: in C++11 destructors default to 'noexcept'
If cleanup can fail, provide an explicit close()/release() that reports the failure, and
have the destructor do a best-effort cleanup.
-
If a class owns a resource, write the destructor — or better, own through a member that already has one (
std::string,std::vector,std::unique_ptr). That is the Rule of Zero, and it is the goal. -
Do not
deletein a destructor by hand if you can help it. Every manualdeleteis a lifetime decision that a smart pointer would have made correctly. -
A polymorphic base needs a
virtualdestructor. Without it,deletethrough a base pointer runs only the base destructor — silently, with no warning. See Polymorphism. -
A destructor is not a place for new work. It runs during stack unwinding and at the end of
main, when there is no one left to handle a failure.
Construction and destruction counters¶
When you are unsure how many objects exist, count them. It is five lines and it settles the argument.
class Tracked
{
public:
Tracked() { ++alive_; }
~Tracked() { --alive_; }
static int alive() { return alive_; }
private:
inline static int alive_ = 0;
};
Use it when a temporary surprises you, when a copy appears from nowhere, or when a leak is suspected and no sanitizer is available.
Checklist¶
- [ ] Any constructor suppresses the default one — did I add
= defaultwhere I need it? - [ ] Are the initialiser list and the declaration order the same?
- [ ] Is every one-argument constructor
explicitunless conversion is intended? - [ ] Do I know the exact construction and destruction order for this class?
- [ ] Does the class need a destructor at all, or can it follow the Rule of Zero?
- [ ] Is the base class's destructor virtual if anything deletes through base pointers?
Related: Rule of Zero, Three and Five · Polymorphism · Class Design · Module 3 — Constructors and Destructors