Part A · Object-oriented programming · Chapter 1
Classes, objects and object lifetime
A class bundles data with the rules that keep that data valid, and an object is a region of memory that obeys those rules from its constructor to its destructor. This chapter covers how objects are laid out, how they are built and destroyed, and which special member functions the compiler writes for you. Interviewers use these questions to find out whether you can write a resource-owning class that does not leak, double free or copy by accident.
Classes and objects
A class is a type: a description of what data an object holds and which operations are allowed on it. An object is an instance of that type, a block of bytes with a lifetime. The class exists only at compile time. At run time there are just objects and the machine code of their member functions.
The two keywords class and struct define the same kind of thing. The only differences are defaults: members of a struct are public by default and it inherits publicly by default, while a class is private on both counts. By convention, struct is used for plain bundles of data with no invariant, and class for types that protect an invariant.
What an object looks like in memory
Only non-static data members take space in an object. Member functions live once in the code segment and are shared by all objects. They receive the object's address as a hidden this parameter. Static data members live once per program, outside every object. A class with virtual functions also carries a hidden pointer to its vtable (Ch 2).
Members are stored in declaration order, and each one is placed at an offset that is a multiple of its alignment. The compiler inserts padding bytes to satisfy this, and it pads the end of the object so that in an array every element is aligned too. So the order in which you declare members can change sizeof:
#include <cstdio>
struct A { char c; int i; char d; };
struct B { int i; char c; char d; };
struct Empty {};
struct WithFn { int x; void f() {} static int count; };
int main() {
std::printf("A=%zu B=%zu Empty=%zu WithFn=%zu\n",
sizeof(A), sizeof(B), sizeof(Empty), sizeof(WithFn));
}
A=12 B=8 Empty=1 WithFn=4
A the int must start at offset 4, so 3 bytes are wasted after c and 3 more at the end. Putting the largest member first, as B does, packs the two chars together and saves 4 bytes per object.An empty class still has size 1, because two distinct objects must have distinct addresses. When an empty class is used as a base class, the empty base optimisation lets it take zero bytes. C++20 [[no_unique_address]] gives members the same freedom.
Know this cold
Functions, static members and type information add nothing to sizeof. Non-static data members, padding and (for polymorphic classes) the vptr are the only things that do.
Encapsulation and invariants
An invariant is a condition that every object of a class must satisfy whenever outside code can see it. Examples: a fraction's denominator is never zero, a vector's size never exceeds its capacity, a string's buffer is always null-terminated. Encapsulation means making the data private, so that the only way to change it is through member functions that preserve the invariant. The constructor establishes the invariant, every public member function keeps it, and the destructor releases what the object owns.
| Access | Who can use the member |
|---|---|
public | Any code |
protected | Members and friends of the class, and members of derived classes (through a derived object) |
private | Members and friends of the class only |
Access control is per class, not per object. A member function can read the private members of any object of its own class, which is how copy constructors and operator== read the other object's data. Access control is checked at compile time only. It has no run-time cost and is no security boundary, because a pointer cast can still reach the bytes.
Interview trap
A getter and a setter for every field is not encapsulation. If setDenominator(0) is allowed, the invariant is gone and the class is a struct with extra typing. Expose operations that mean something (add, push_back, withdraw) and validate inside them.
Constructors
A constructor turns raw storage into a valid object. It has the class's name, has no return type, and runs automatically when the object is created. A class can have several constructors, distinguished by their parameters.
| Kind | Signature | Called by |
|---|---|---|
| Default | T() (or every parameter defaulted) | T t; T t{}; new T, arrays of T |
| Parameterised | T(int, std::string) | T t(1, "x"); T t{1, "x"}; |
| Copy | T(const T&) | T b = a; T b(a);, pass or return an lvalue by value |
| Move | T(T&&) noexcept | T b = std::move(a);, from temporaries when not elided (Ch 20) |
| Converting | any non-explicit constructor callable with one argument | implicit conversions such as T t = 5; |
| Delegating | T() : T(0, "") {} | a constructor that forwards to another one of the same class |
explicit and implicit conversions
A constructor callable with one argument doubles as an implicit conversion. That is useful for std::string s = "hi"; and dangerous for Meters m = 5.0; or a function void sleep(Seconds) called as sleep(5). Mark single-argument constructors explicit unless the conversion is lossless and obvious. An explicit constructor still works with direct initialisation (Meters m(5.0) or Meters m{5.0}), but it is rejected for copy initialisation (Meters m = 5.0) and for implicit argument conversion.
= default, = delete and default member initialisers
= default asks the compiler for its usual implementation, and = delete makes any call ill-formed. A deleted function still takes part in overload resolution, so void f(double); void f(int) = delete; blocks f(3) instead of silently converting. A default member initialiser (int count = 0;) is used by every constructor that does not mention that member in its initialiser list, which removes a whole class of "forgot to initialise in one constructor" bugs.
class Connection {
public:
Connection() : Connection("localhost", 80) {}
Connection(std::string host, int port) : host_(std::move(host)), port_(port) {}
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
private:
std::string host_;
int port_;
int retries_ = 3;
};
Interview trap
T t(); does not create an object. It declares a function named t that returns a T (the "most vexing parse"). Write T t; or T t{}; instead.
Member initialiser lists and construction order
A constructor body runs after every base and member has been constructed. So assigning to members inside the body means constructing them first (default constructor) and then assigning (assignment operator). The member initialiser list constructs each member directly with the right value:
Person(std::string name) : name_(std::move(name)), id_(next_id++) {} // construct once
Person(std::string name) { name_ = name; id_ = next_id++; } // default-construct, then assign
Some members must go in the initialiser list, because they cannot be assigned or default-constructed afterwards: const members, reference members, members of a type with no default constructor, and base classes that need arguments.
The order is fixed by the class, not by the list
Construction always happens in this order:
- Virtual base classes (only in the most-derived class).
- Direct base classes, in the order they appear in the base-specifier list.
- Non-static data members, in declaration order.
- The constructor body.
Destruction runs exactly in reverse. The order in which you write the initialiser list is ignored. GCC and Clang warn about a mismatch with -Wreorder, which -Wall enables. The order matters whenever one member's initialiser reads another member.
This program traces the whole lifecycle:
#include <cstdio>
struct Tracer {
const char* name;
explicit Tracer(const char* n) : name(n) { std::printf("ctor %s\n", name); }
~Tracer() { std::printf("dtor %s\n", name); }
};
struct Base {
Base() { std::puts("ctor Base"); }
~Base() { std::puts("dtor Base"); }
};
struct Widget : Base {
Tracer a{"a"};
Tracer b;
Widget() : b("b") { std::puts("Widget body"); }
~Widget() { std::puts("~Widget body"); }
};
int main() {
Widget w;
std::puts("end of main");
}
ctor Base ctor a ctor b Widget body end of main ~Widget body dtor b dtor a dtor Base
Destructors and RAII
A destructor (~T()) runs when an object's lifetime ends. It takes no arguments, cannot be overloaded, and is implicitly noexcept. When it runs depends on the storage duration:
| Object | Destroyed |
|---|---|
| Local variable | At the end of its scope, in reverse order of construction, including during stack unwinding from an exception |
| Temporary | At the end of the full expression, unless it is bound to a const& or && local, which extends its life to the reference's scope |
| Heap object | Only when you delete it (or a smart pointer does) |
| Array element | In reverse index order |
| Static or global | After main returns, in reverse order of construction |
| Member or base | After the enclosing object's destructor body, in reverse order |
RAII
Resource Acquisition Is Initialisation ties a resource (memory, a file, a lock, a socket) to an object's lifetime: acquire it in the constructor and release it in the destructor. Because destructors of locals run on every exit path, including return, break and exceptions, the release cannot be forgotten. std::unique_ptr, std::lock_guard, std::fstream and std::vector are all RAII types.
When a constructor throws
An object whose constructor did not finish never existed, so its destructor does not run. The members and bases that had already been fully constructed are destroyed, in reverse order. So a class that grabs two raw resources in its constructor leaks the first one if acquiring the second one throws. The fix is to hold each resource in its own RAII member, such as a unique_ptr, so that each member cleans itself up.
class Bad {
int* a_; int* b_;
public:
Bad() : a_(new int[100]), b_(new int[100]) {} // if the second new throws, a_ leaks
~Bad() { delete[] a_; delete[] b_; }
};
class Good {
std::unique_ptr<int[]> a_ = std::make_unique<int[]>(100);
std::unique_ptr<int[]> b_ = std::make_unique<int[]>(100); // a_ is destroyed if this throws
};
Interview trap
Never let an exception escape a destructor. Destructors are noexcept by default, so a throw calls std::terminate. Even with noexcept(false), throwing while another exception is unwinding the stack terminates the program.
The special member functions
The compiler can write six functions for you: the default constructor, the destructor, the copy constructor, copy assignment, the move constructor and move assignment. The generated versions work member by member. They copy, move, assign or destroy each base and member in order. Whether each one is generated depends on what you declared yourself:
| You declare | default ctor | destructor | copy ctor | copy assign | move ctor | move assign |
|---|---|---|---|---|---|---|
| nothing | defaulted | defaulted | defaulted | defaulted | defaulted | defaulted |
| any constructor | not declared | defaulted | defaulted | defaulted | defaulted | defaulted |
| destructor | defaulted | yours | defaulted* | defaulted* | not declared | not declared |
| copy ctor | not declared | defaulted | yours | defaulted* | not declared | not declared |
| copy assign | defaulted | defaulted | defaulted* | yours | not declared | not declared |
| move ctor | not declared | defaulted | deleted | deleted | yours | not declared |
| move assign | defaulted | defaulted | deleted | deleted | not declared | yours |
* Generated, but deprecated since C++11: rely on it and a future standard may break your code. "Not declared" for a move operation is subtle. The class still works with std::move, but overload resolution then picks the copy operation, so a "move" silently copies.
Know this cold
Declaring a destructor, even an empty one or ~T() = default, suppresses the implicit move operations. Declaring any move operation deletes both copy operations.
The rules of three, five and zero
If a class needs a hand-written destructor, it almost certainly owns a resource, and then the generated copy operations are wrong: they copy the pointer, not what it points to (a shallow copy), so two objects free the same memory.
- Rule of three (C++98): if you write one of destructor, copy constructor or copy assignment, write all three.
- Rule of five (C++11): add the move constructor and move assignment, or the type will copy where it could move.
- Rule of zero: better still, write none of them. Hold resources in members that already manage themselves (
std::string,std::vector,std::unique_ptr), and let the compiler generate correct code. Only classes whose whole job is managing one resource should implement the five.
A rule-of-five class with copy-and-swap
The copy-and-swap idiom writes assignment once, taking its argument by value. The parameter is built by the copy constructor (from an lvalue) or by the move constructor (from an rvalue), and then swapped in. The old contents are released when the parameter is destroyed. This gives the strong exception guarantee and handles self-assignment for free.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t n) : size_(n), data_(new int[n]()) {}
~Buffer() { delete[] data_; }
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
Buffer(Buffer&& other) noexcept
: size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {}
Buffer& operator=(Buffer other) noexcept {
swap(other);
return *this;
}
void swap(Buffer& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}
int& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
private:
std::size_t size_;
int* data_;
};
int main() {
Buffer a(3);
a[0] = 7;
Buffer b = a;
b[0] = 9;
Buffer c(1);
c = a;
c = std::move(b);
std::printf("%d %d %zu %zu\n", a[0], c[0], c.size(), b.size());
}
7 9 3 0
Note that size_ is declared before data_, and that the copy constructor's new int[other.size_] reads other, not its own size_. Both choices keep the code safe if someone reorders the members later.
Tip
The by-value operator= costs one extra move when you assign from an rvalue, compared with a separate operator=(Buffer&&), and it always allocates on copy even when the existing buffer is big enough. std::vector avoids both costs by writing the operators separately. Mention this trade-off when you show the idiom.
Concept and tricky questions
ConceptWhat is the difference between struct and class in C++?
Answer
Answer. The defaults are the only difference. A struct has public members and public inheritance by default, while a class has private members and private inheritance by default.
Why. Both can have constructors, virtual functions, templates and access specifiers. Candidates often say "structs cannot have member functions", which is true in C and false in C++. Convention: use struct for passive data with no invariant, and class when you protect one.
ConceptWhich members must be initialised in the member initialiser list rather than in the constructor body?
Answer
Answer. const data members, reference members, members whose type has no default constructor, and base classes that need constructor arguments.
Why. By the time the body runs, every member already exists. A const or a reference cannot be assigned afterwards, and a type without a default constructor could not have been built in the first place. For everything else, the list is still preferred, because it constructs once instead of default-constructing and then assigning.
ConceptWhen does the compiler not generate a default constructor?
Answer
Answer. In two cases. It is not declared when you declare any constructor yourself, including a copy or move constructor. It is defined as deleted when some member or base cannot be default-constructed: a reference member without a default member initialiser, a const member of a type that would be left uninitialised, or a member whose own default constructor is deleted or inaccessible.
Why. The compiler assumes that a class with its own constructor has rules for building objects. T() = default; brings the default constructor back.
ConceptWhat is RAII, and why does it make code exception safe?
Answer
Answer. RAII binds a resource to an object: the constructor acquires the resource and the destructor releases it. Destructors of local objects run on every way out of a scope, including stack unwinding after an exception, so the resource is always released.
Why. Manual cleanup has to be repeated on every return and in every catch, and that is where leaks come from. With RAII, cleanup is written once. Examples: unique_ptr (memory), lock_guard (a mutex), fstream (a file), jthread (joining a thread).
ConceptState the rule of three, the rule of five and the rule of zero. Which one should you follow by default?
Answer
Answer. Rule of three: if you write the destructor, the copy constructor or copy assignment, write all three. Rule of five: add the move constructor and move assignment too. Rule of zero: write none, by holding resources in members that already manage themselves. Rule of zero is the default.
Why. A hand-written destructor means the class owns something, so the member-wise copies the compiler generates would share it. Writing the five correctly is error-prone, so confine it to small resource handles (one per resource type) and compose everything else from them.
ConceptWhat is the difference between a shallow copy and a deep copy?
Answer
Answer. A shallow copy duplicates the pointer, so both objects point to the same buffer. A deep copy allocates a new buffer and copies the contents, so each object owns its own.
Why. The implicit copy constructor copies each member, and copying a raw pointer member is a shallow copy. For an owning class that leads to a double free when both destructors run, and to changes through one object showing up in the other. Shallow sharing is correct only when it is intended and reference counted, as in shared_ptr.
TrickyCan a member function access the private members of a different object of the same class?
Answer
Answer. Yes. Access control in C++ is per class, not per object.
Why. That is how a copy constructor reads other.data_ and how operator== compares private fields. People who answer "no" are usually thinking of object-level privacy in languages like Smalltalk or Ruby. The check happens at compile time, on the name being accessed and the context it is accessed from.
TrickyWhat is sizeof an empty class, and why isn't it 0? Is it ever 0?
Answer
Answer. It is at least 1 (in practice exactly 1). A complete object never has size 0, but an empty base subobject can take zero bytes (the empty base optimisation), and so can a member marked [[no_unique_address]] (C++20).
Why. Distinct objects need distinct addresses: in Empty arr[2], &arr[0] != &arr[1] requires each element to take space. The EBO is why std::unique_ptr<T> with the default deleter is the size of a raw pointer.
TrickyA constructor throws halfway through. Does the destructor run? What happens to the members that were already built?
Answer
Answer. The class's destructor does not run. Every base and member that was already fully constructed is destroyed, in reverse order, and then the exception propagates. If the object was being created with new, the memory is freed too.
Why. The object's lifetime begins only when its constructor completes. Raw resources acquired in the constructor body leak in this case, which is the reason to hold each resource in an RAII member. A function-try-block on the constructor can observe or translate the exception, but it cannot stop it from propagating.
TrickyWhy must a copy constructor take its parameter by reference? What happens with T(T other)?
Answer
Answer. T(T) is ill-formed and the compiler rejects it. If it were allowed, passing the argument by value would itself need a copy, which would call the copy constructor, which would need another copy, forever.
Why. The standard says such a constructor is never a copy constructor and is ill-formed ([class.copy.ctor]). The usual answer "infinite recursion at run time" is the motivation, but in practice you get a compile error. The parameter is const T& so that temporaries and const objects can be copied. T(T&) is also a legal copy constructor, but it cannot copy const objects.
TrickyDoes T b = a; call the copy constructor or copy assignment?
Answer
Answer. The copy constructor. The = in a declaration is initialisation syntax, not assignment. Assignment happens only to an object that already exists: T b; b = a;.
Why. The rule of thumb: if a new object is being created, a constructor runs. Assignment operators run only on objects that are already alive, and they must release whatever the object held before.
TrickyCan a destructor be private? Why would you want that?
Answer
Answer. Yes. Objects with automatic, static or member storage then cannot be created outside the class, because their destructor would be called implicitly from code that has no access to it. Code can only create them on the heap and must destroy them through a member or friend, for example a destroy() function or reference counting.
Why. This is used to force heap-only objects, such as intrusively reference-counted COM-style objects that delete themselves when the count reaches zero. A protected destructor in a base class is the other common case: it stops delete through a base pointer without making the destructor virtual (Ch 2).
ConceptWhat does explicit do on a constructor, and when should you leave it off?
Answer
Answer. explicit stops the constructor from being used for implicit conversions and for copy initialisation (T t = x;). Direct initialisation (T t(x) or T t{x}) still works. Leave it off only when the conversion is natural and lossless, as with std::string from const char* or a Fraction from an integer.
Why. Implicit conversions let calls like sleep(5) compile against sleep(Duration) with the wrong unit. Since C++11, explicit also matters for multi-argument constructors, because it blocks return {a, b}; and f({a, b}). C++20 adds explicit(bool) for conditional explicitness in templates.
TrickyYou add an empty destructor ~Holder() {} to a class whose only member is a std::vector. What changes?
Answer
Answer. The implicit move constructor and move assignment are no longer declared, so std::move on a Holder now copies the vector. The class also stops being trivially destructible. Program behaviour is unchanged, but performance can collapse, for example when a std::vector<Holder> reallocates.
Why. A user-declared destructor suggests resource management, so C++11 kept the old copying behaviour for safety. ~Holder() = default; still counts as user-declared and has the same effect. Either delete the destructor or add Holder(Holder&&) = default; and friends (and then the copy operations too, since declaring a move deletes them). The Holder drill in the next section shows the effect.
Predict the output and find the bug
OutputWhat does this print? Note the order of the initialiser list.
#include <cstdio>
struct Part {
const char* name;
Part(const char* n) : name(n) { std::printf("+%s ", name); }
~Part() { std::printf("-%s ", name); }
};
struct Base {
Base() { std::printf("+Base "); }
~Base() { std::printf("-Base "); }
};
struct Car : Base {
Part engine;
Part wheel{"wheel"};
Car() : wheel("spare"), engine("engine") { std::printf("+Car "); }
~Car() { std::printf("-Car "); }
};
int main() {
Car car;
std::printf("| ");
}
Answer
+Base +engine +spare +Car | -Car -spare -engine -Base
Why. The base comes first, then the members in declaration order (engine, then wheel), whatever order the initialiser list uses. Because wheel appears in the initialiser list, its default member initialiser {"wheel"} is ignored and "spare" is used. Destruction is the exact reverse. Both compilers warn with -Wreorder.
OutputWhich special member functions run, and in what order?
#include <cstdio>
struct T {
T() { std::puts("default"); }
T(const T&) { std::puts("copy ctor"); }
T& operator=(const T&) { std::puts("copy assign"); return *this; }
};
int main() {
T a;
T b = a;
T c(a);
b = c;
T d = T();
}
Answer
default copy ctor copy ctor copy assign default
Why. T b = a; creates a new object, so the copy constructor runs, not assignment. b = c; assigns to an existing object. T d = T(); runs only the default constructor: since C++17, a prvalue initialises d directly, with no temporary and no copy (guaranteed copy elision, Ch 23).
OutputWhen does each temporary die?
#include <cstdio>
struct Noisy {
int id;
Noisy(int i) : id(i) { std::printf("make %d\n", id); }
~Noisy() { std::printf("drop %d\n", id); }
};
int main() {
Noisy a(1);
Noisy(2);
const Noisy& r = Noisy(3);
std::printf("end %d\n", r.id);
}
Answer
make 1 make 2 drop 2 make 3 end 3 drop 3 drop 1
Why. Noisy(2); creates a temporary that is destroyed at the end of its full expression, the same line. Binding Noisy(3) to a const& local extends its lifetime to the end of r's scope. Locals are destroyed in reverse order of creation, so the extended temporary (created after a) dies first. Watch out for Noisy(x); with an identifier: that line declares a variable named x.
OutputHow many objects get constructed?
#include <cstdio>
struct P {
P() { std::puts("P()"); }
};
int main() {
P a;
P b();
P c{};
std::puts("done");
}
Answer
P() P() done
Why. Only two objects, a and c. P b(); is the most vexing parse: it declares a function b that takes no arguments and returns a P. Brace initialisation (P c{};) cannot be parsed as a function declaration, which is one reason to prefer it. GCC warns with -Wvexing-parse.
OutputWhat are the two sizes after the "move"?
#include <cstdio>
#include <utility>
#include <vector>
struct Holder {
std::vector<int> v;
~Holder() {}
};
int main() {
Holder a{{1, 2, 3}};
Holder b = std::move(a);
std::printf("a=%zu b=%zu\n", a.v.size(), b.v.size());
}
Answer
a=3 b=3
Why. The user-declared destructor suppresses the implicit move constructor. std::move(a) produces an rvalue, but the only constructor that can take it is the implicit copy constructor (const Holder& binds to rvalues), so the vector is copied. Remove the destructor and the output becomes a=0 b=3 with both libstdc++ and libc++. (A moved-from vector is formally "valid but unspecified", but both implementations leave it empty.)
Find the bugThe author expects 10 15. What does the program actually do?
#include <cstdio>
struct Range {
int end;
int begin;
Range(int b, int len) : begin(b), end(begin + len) {}
};
int main() {
Range r(10, 5);
std::printf("%d %d\n", r.begin, r.end);
}
Answer
Answer. The behaviour is undefined. end is declared first, so it is initialised first, and begin + len reads begin before it has been initialised. The program can print any value for end.
Fix. Declare begin before end, or initialise end from the parameters: end(b + len). The second fix is more robust, because it does not depend on member order at all. -Wall warns about both the reordering and the uninitialised read.
Find the bugThis small string class works in a quick test and crashes in production. Why?
#include <cstring>
struct Str {
char* p;
Str(const char* s) : p(new char[std::strlen(s) + 1]) { std::strcpy(p, s); }
~Str() { delete[] p; }
};
int main() {
Str a("hi");
Str b = a;
}
Answer
Answer. It is a double free. The implicit copy constructor copies the pointer, so a.p == b.p. Both destructors delete[] the same buffer. AddressSanitizer reports attempting double-free.
Fix. Follow the rule of three/five: write a deep-copying copy constructor and copy assignment (and the moves), or delete copying. The best fix is the rule of zero: hold a std::string.
Find the bugThis assignment operator passes every test except one. Which input breaks it?
#include <cstdio>
#include <cstring>
class Name {
public:
Name(const char* s) : p_(new char[std::strlen(s) + 1]) { std::strcpy(p_, s); }
Name(const Name& o) : Name(o.p_) {}
Name& operator=(const Name& o) {
delete[] p_;
p_ = new char[std::strlen(o.p_) + 1];
std::strcpy(p_, o.p_);
return *this;
}
~Name() { delete[] p_; }
const char* get() const { return p_; }
private:
char* p_;
};
int main() {
Name n("alice");
Name& alias = n;
n = alias;
std::puts(n.get());
}
Answer
Answer. Self-assignment. When &o == this, delete[] p_ frees the buffer that o.p_ still points to, and then strlen(o.p_) reads freed memory: a use after free. ASan reports heap-use-after-free. Self-assignment usually arrives through aliases such as v[i] = v[j], not as literal n = n.
Fix. Allocate and copy first, then delete. That also gives the strong exception guarantee if new throws. Alternatively, use copy-and-swap. An if (this == &o) check alone fixes self-assignment but still leaves the object broken if new throws.
Find the bugWhy does this not compile, and is that a bug or a feature?
struct Meters {
explicit Meters(double v) : value(v) {}
double value;
};
static double twice(Meters m) { return 2 * m.value; }
int main() {
Meters a(3.0);
Meters b = 4.0;
return static_cast<int>(twice(a) + twice(5.0) + b.value);
}
Answer
Answer. There are two errors, and both are the intended effect of explicit. Meters b = 4.0; is copy initialisation, which cannot use an explicit constructor. twice(5.0) would need an implicit conversion from double to Meters. Meters a(3.0) is direct initialisation and is fine.
Fix. Be explicit at the call site: Meters b{4.0}; and twice(Meters{5.0}). Unit types are the textbook case for explicit, because a bare 5.0 could mean feet.
Write the code
CodeImplement a String class that owns a heap buffer and follows the rule of five.
Requirements: a constructor from const char* (default ""), a deep-copying copy constructor and copy assignment, a noexcept move constructor and move assignment that leave the source empty, size(), and c_str(), which must work on a moved-from object. Self-assignment and self-move must be safe.
Answer
Approach. Store size_ and a char*, with size declared first because the buffer allocation reads it. The copy constructor allocates and copies size_ + 1 bytes. Moves steal the pointer with std::exchange. Both assignments build a local copy or moved temporary and swap it in, so self-assignment needs no special handling and the old buffer is freed by the temporary's destructor.
#include <cstdio>
#include <cstring>
#include <utility>
class String {
public:
String(const char* s = "") : size_(std::strlen(s)), data_(new char[size_ + 1]) {
std::memcpy(data_, s, size_ + 1);
}
String(const String& other) : size_(other.size_), data_(new char[other.size_ + 1]) {
std::memcpy(data_, other.data_, size_ + 1);
}
String(String&& other) noexcept
: size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {}
String& operator=(const String& other) {
String copy(other);
swap(copy);
return *this;
}
String& operator=(String&& other) noexcept {
String moved(std::move(other));
swap(moved);
return *this;
}
~String() { delete[] data_; }
void swap(String& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}
std::size_t size() const { return size_; }
const char* c_str() const { return data_ ? data_ : ""; }
private:
std::size_t size_;
char* data_;
};
int main() {
String a("hello");
String b = a;
String c;
c = a;
String d = std::move(b);
String& alias = a;
a = alias;
String& self = c;
c = std::move(self);
std::printf("%s %s %s [%s] %zu\n", a.c_str(), c.c_str(), d.c_str(), b.c_str(), b.size());
}
hello hello hello [] 0
Complexity and pitfalls. Copies are O(n) and moves are O(1). Common mistakes: forgetting the + 1 for the terminator, leaving the moved-from pointer non-null (double free), returning nullptr from c_str() after a move, and omitting noexcept. Without noexcept, std::vector<String> copies on reallocation (Ch 20).
CodeWrite a class Counted that reports how many objects are alive now and how many were ever created.
Every way of creating an object (default, copy, move) must count. Assignment creates nothing. Counted::live() must return 0 once every object has been destroyed.
Answer
Approach. Use two inline static counters. Every constructor increments both, and the destructor decrements live_. The trap is the implicit copy and move constructors, which would create objects without counting them and drive live() negative. So all three constructors are written by hand, and the assignments are defaulted, because they do not change the number of objects.
#include <cstdio>
#include <utility>
class Counted {
public:
Counted() { ++live_; ++created_; }
Counted(const Counted&) { ++live_; ++created_; }
Counted(Counted&&) noexcept { ++live_; ++created_; }
Counted& operator=(const Counted&) = default;
Counted& operator=(Counted&&) noexcept = default;
~Counted() { --live_; }
static int live() { return live_; }
static int created() { return created_; }
private:
inline static int live_ = 0;
inline static int created_ = 0;
};
int main() {
{
Counted a;
Counted b = a;
Counted c = std::move(b);
a = c;
std::printf("inside: live=%d created=%d\n", Counted::live(), Counted::created());
}
std::printf("after: live=%d created=%d\n", Counted::live(), Counted::created());
}
inside: live=3 created=3 after: live=0 created=3
Pitfalls. Defining only the default constructor gives after: live=-2. For use across threads, make the counters std::atomic<int>. In a CRTP base, Counted<T> gives every derived type its own counters.
CodeWrite an RAII wrapper File for C's FILE*.
It must open the file in its constructor and close it in its destructor, be non-copyable and movable, and offer explicit operator bool and get(). Moving must transfer ownership, so the file is closed exactly once.
Answer
Approach. Delete both copy operations. The move constructor takes the handle with std::exchange(other.f_, nullptr). Move assignment closes its current handle before taking the new one, and guards against self-move. The destructor closes only a non-null handle.
#include <cstdio>
#include <utility>
class File {
public:
File(const char* path, const char* mode) : f_(std::fopen(path, mode)) {}
~File() { if (f_) std::fclose(f_); }
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept : f_(std::exchange(other.f_, nullptr)) {}
File& operator=(File&& other) noexcept {
if (this != &other) {
if (f_) std::fclose(f_);
f_ = std::exchange(other.f_, nullptr);
}
return *this;
}
explicit operator bool() const { return f_ != nullptr; }
std::FILE* get() const { return f_; }
private:
std::FILE* f_;
};
int main() {
{
File out("ch01_demo.txt", "w");
std::fputs("raii\n", out.get());
}
File in("ch01_demo.txt", "r");
char line[16] = {};
if (!std::fgets(line, sizeof line, in.get())) return 1;
File moved = std::move(in);
std::printf("%s", line);
std::printf("in=%d moved=%d\n", static_cast<bool>(in), static_cast<bool>(moved));
std::remove("ch01_demo.txt");
}
raii in=0 moved=1
Pitfalls. The inner block matters: the writer's destructor flushes and closes the file before it is reopened for reading. In production, prefer std::unique_ptr<FILE, decltype(&fclose)> or a stateless deleter (Ch 12), or simply std::fstream. Also decide what a failed fopen means: here it gives a null handle that converts to false, and throwing is the other option.
CodeImplement IntVector, a growable array of int with the rule of five.
Support push_back with amortised O(1) growth (doubling), operator[], size() and capacity(), deep copy, and O(1) noexcept moves. A default-constructed vector must not allocate.
Answer
Approach. Store three members with default member initialisers, so the defaulted default constructor yields an empty vector. When the vector is full, grow() allocates double the capacity, copies, and frees the old buffer. Copying allocates exactly size elements. Assignment is copy-and-swap.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <utility>
class IntVector {
public:
IntVector() = default;
IntVector(const IntVector& other)
: data_(other.size_ ? new int[other.size_] : nullptr), size_(other.size_), cap_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
IntVector(IntVector&& other) noexcept
: data_(std::exchange(other.data_, nullptr)),
size_(std::exchange(other.size_, 0)),
cap_(std::exchange(other.cap_, 0)) {}
IntVector& operator=(IntVector other) noexcept {
swap(other);
return *this;
}
~IntVector() { delete[] data_; }
void push_back(int value) {
if (size_ == cap_) grow();
data_[size_++] = value;
}
void swap(IntVector& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
std::swap(cap_, other.cap_);
}
int operator[](std::size_t i) const { return data_[i]; }
std::size_t size() const { return size_; }
std::size_t capacity() const { return cap_; }
private:
void grow() {
std::size_t next = cap_ ? 2 * cap_ : 1;
int* bigger = new int[next];
std::copy(data_, data_ + size_, bigger);
delete[] data_;
data_ = bigger;
cap_ = next;
}
int* data_ = nullptr;
std::size_t size_ = 0;
std::size_t cap_ = 0;
};
int main() {
IntVector a;
for (int i = 1; i <= 5; ++i) a.push_back(i);
IntVector b = a;
b.push_back(6);
IntVector c;
c = std::move(b);
std::printf("a size=%zu cap=%zu\n", a.size(), a.capacity());
std::printf("b size=%zu\n", b.size());
std::printf("c:");
for (std::size_t i = 0; i < c.size(); ++i) std::printf(" %d", c[i]);
std::printf("\n");
}
a size=5 cap=8 b size=0 c: 1 2 3 4 5 6
Complexity and pitfalls. Doubling makes n pushes cost O(n) copies in total, so each push is amortised O(1). Growing by a constant amount instead would make n pushes cost O(n²). grow() allocates before it frees, so a throwing new leaves the vector unchanged. A generic Vector<T> would need raw storage and placement new so that capacity does not construct elements (Ch 14).
CodeDesign a Date class that can never hold an invalid date.
A Date has a year, month and day, and must always be a real calendar date (leap years included: 2000 is a leap year, 1900 is not). Invalid input is common and expected, so report it without exceptions: provide static std::optional<Date> make(int y, int m, int d), and make it impossible to construct a Date any other way, except by copying one. Add read-only accessors and next_day(), which must keep the invariant.
Answer
Approach. This is the named constructor idiom. The real constructor is private and trusts its arguments, and the public static make validates first, so every Date in the program went through the check. A constructor cannot return "no object", which is why the factory returns std::optional; throwing from a public constructor is the other option when bad input is truly exceptional. There are no setters: next_day() is the only mutation, and it rolls the day, month and year over in that order so the invariant holds on exit.
#include <cstdio>
#include <optional>
class Date {
public:
static std::optional<Date> make(int y, int m, int d) {
if (m < 1 || m > 12 || d < 1 || d > days_in(y, m)) return std::nullopt;
return Date(y, m, d);
}
int year() const { return y_; }
int month() const { return m_; }
int day() const { return d_; }
void next_day() {
if (++d_ <= days_in(y_, m_)) return;
d_ = 1;
if (++m_ <= 12) return;
m_ = 1;
++y_;
}
private:
Date(int y, int m, int d) : y_(y), m_(m), d_(d) {}
static bool leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; }
static int days_in(int y, int m) {
static constexpr int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
return m == 2 && leap(y) ? 29 : days[m - 1];
}
int y_, m_, d_;
};
static void show(const Date& d) { std::printf("%04d-%02d-%02d\n", d.year(), d.month(), d.day()); }
int main() {
const int tests[][3] = {{2024, 2, 29}, {2023, 2, 29}, {1900, 2, 29}, {2000, 2, 29}, {2024, 13, 1}};
for (const auto& t : tests) {
auto d = Date::make(t[0], t[1], t[2]);
std::printf("%d-%d-%d %s\n", t[0], t[1], t[2], d ? "ok" : "rejected");
}
Date d = *Date::make(2023, 12, 31);
d.next_day();
show(d);
d = *Date::make(2024, 2, 28);
d.next_day();
show(d);
d.next_day();
show(d);
// Date bad(2024, 2, 30); // error: the constructor is private
}
2024-2-29 ok 2023-2-29 rejected 1900-2-29 rejected 2000-2-29 ok 2024-13-1 rejected 2024-01-01 2024-02-29 2024-03-01
Pitfalls. The leap-year rule has three parts (divisible by 4, except centuries, except multiples of 400), and forgetting the last two accepts 1900-02-29. The implicit copy operations are correct here, because copying a valid date gives a valid date, so the rule of zero applies. std::optional<Date> can still hold a Date built by a private constructor because make, a member, builds it. Dereferencing an empty optional with * is undefined behaviour, so production callers check it or use .value(), which throws. A one-line struct with three public ints would be simpler and would let any caller write d.day = 31 in February.
Cheat sheet
structandclassdiffer only in default access and default inheritance.sizeofcounts data members, padding and the vptr. Order members from largest to smallest to reduce padding. An empty class has size 1, except as a base (EBO).- Construction order: virtual bases, bases, members in declaration order, then the body. Destruction is the reverse. The initialiser-list order is ignored.
constmembers, references, members without a default constructor and bases with arguments must be in the initialiser list.- If a constructor throws, the destructor does not run, but the members already built are destroyed. Hold each resource in its own RAII member.
- A user-declared destructor or copy operation suppresses the implicit moves (so
std::movecopies). A user-declared move deletes the copies. - Follow the rule of zero by default. Write the five only for a class whose single job is owning one resource, and make its moves
noexcept. T b = a;is construction, not assignment.T t();declares a function. Mark single-argument constructorsexplicitunless the conversion is lossless.