Classes & Constructors
What the compiler writes for you, and where that stops being enough.
Beginner
Animal a; to compile.
Declaring a destructor also suppresses the implicit move constructor/assignment (see
Move Semantics for the rule of five this leads to).
const, or lack a default constructor. There's also a shadowing trap
worth knowing about explicitly:
class Point {
int x, y, z;
public:
Point(int x, int y, int z) : x(x) { // only x is initialized via the list
y = y; // BUG: assigns the parameter y to itself — the member y is never touched
this->z = z; // correct — this-> disambiguates the member from the parameter
}
};
y = y inside the body resolves to the parameter on both sides
— the member is left uninitialized. Either put every parameter-named member in the
initialization list, or use this-> explicitly in the body.
std::string members, standing in
for anything that needs real copy semantics) written out with all three rule-of-three
members plus the two rule-of-five move members:
class Animal {
std::string name, sound;
public:
Animal() : name("unknown"), sound("unknown") {} // default ctor
Animal(std::string name, std::string sound) : name{name}, sound{sound} {} // parameterized ctor
Animal(const Animal &rhs) : name(rhs.name), sound(rhs.sound) {} // copy ctor
Animal & operator=(const Animal &rhs) { // copy assignment
if (this != &rhs) { name = rhs.name; sound = rhs.sound; }
return *this;
}
Animal(Animal &&rhs) noexcept // move ctor
: name(std::move(rhs.name)), sound(std::move(rhs.sound)) {}
Animal & operator=(Animal &&rhs) noexcept { // move assignment
if (this != &rhs) { name = std::move(rhs.name); sound = std::move(rhs.sound); }
return *this;
}
~Animal() = default; // dtor
};
if (this != &rhs) self-assignment check matters:
without it, a = a would (for a class managing a raw resource) potentially free
the resource before copying from it — copying from memory that was just invalidated.
For members that are themselves well-behaved types like std::string it's mostly
a wasted check rather than a correctness bug, but the habit is worth keeping once a class
manages a raw pointer.
explicit turns that conversion
off, requiring the caller to construct the object on purpose.
class Meters {
int value;
public:
Meters(int value) : value(value) {} // one-argument ctor — an implicit conversion path
};
void report(const Meters &m) { /* ... */ }
Meters m = 5; // implicit int -> Meters conversion — probably not what you meant
report(42); // also compiles — 42 silently becomes a Meters(42)
class Meters {
int value;
public:
explicit Meters(int value) : value(value) {} // no more silent conversion
};
Meters m = 5; // error: no implicit conversion from int to Meters
Meters m(5); // fine — explicit construction
report(Meters(42)); // fine — the caller has to say what they mean
explicit on any single-argument constructor unless you
specifically want the implicit conversion — it's easy to add later if needed, and hard
to track down bugs caused by a conversion nobody meant to invoke.
const. That maximizes how many contexts the class works in (e.g. being
usable through a const& parameter) and lets the compiler flag accidental
mutation as a build error instead of a runtime bug.
BankAccount class that avoids the initialization-list shadowing bug
with an explicit this->, and prints a message from every rule-of-three member so
you can see exactly which one runs for each line in main():
TopNotchNote/cpp/classes_constructors_bankaccount_rule_of_three.cpp
new remains possible —
and even then, nothing can call delete on it from outside the class, so
cleanup has to be handled by a member function.
class HeapOnly {
~HeapOnly() {} // private dtor — stack objects and inheritance are both blocked
public:
HeapOnly() = default;
void destroySelf() { delete this; } // the only way this object can be destroyed
};
HeapOnly a; // error: destructor is private, can't be stack-allocated
HeapOnly *p = new HeapOnly(); // fine — heap allocation doesn't need the destructor at construction time
new T[10] default-constructs each element), and if the
base class in an inheritance hierarchy has no default constructor, the derived
class doesn't get an implicit default constructor either — the derived constructor
would have no way to initialize the base subobject.