Lesson 12 / 14
Exceptions
The measurement of how many times a thrown object is copied while being caught, which parts a construction cut short by an exception destroy, and how a temporary object is copied fewer times than a named local.
Published Updated
In this lesson
- Uses a counter to measure that catching by value produces one more copy than catching by reference
- Observes that an exception that cuts a construction short destroys the already-constructed parts but not the object itself
- Measures how throwing a temporary object instead of a named local reduces the copy count
Contents
The previous four lessons measured constructors, destructors, and virtual calls that
completed in a normal flow. This lesson takes up the case where the flow is cut
short: when a function throws an error (throw), the thrown object can be copied
one or more times before it reaches the catch block; if the interruption happened in
the middle of a constructor, the already-constructed parts are destroyed, but the
object itself is never destroyed. The measurement question: how many times, and with
which object’s copy, does an expression work?
Catching by Value Produces One More Copy Than Catching by Reference
ErrorObject increments a counter in its copy constructor. When we throw the same
named local object and catch it once by value and once by reference, how many copies
does the counter show?
// exception_copy_counter.cpp — catching by value produces one more copy than catching by reference
#include <iostream>
class ErrorObject {
public:
ErrorObject() = default;
ErrorObject(const ErrorObject&) { ++copy_count; }
static int copy_count;
};
int ErrorObject::copy_count = 0;
void throw_it() {
ErrorObject e;
throw e;
}
int main() {
try {
throw_it();
} catch (ErrorObject e) {
(void)e;
}
std::cout << "copy count after catch by value " << ErrorObject::copy_count << "\n";
ErrorObject::copy_count = 0;
try {
throw_it();
} catch (const ErrorObject& e) {
(void)e;
}
std::cout << "copy count after catch by reference " << ErrorObject::copy_count << "\n";
return 0;
}
copy count after catch by value 2 copy count after catch by reference 1
PS13. throw e; throws a named local — it produces one copy into the exception
storage (the first). catch (ErrorObject e) catches it by value, producing a
second copy: total 2. catch (const ErrorObject& e) binds directly to the stored
object, producing no extra copy: total 1. Class copy: catching by value adds
one more copy to the work of catching by reference.
An Exception That Cuts a Construction Short: The Already-Constructed Parts Are Destroyed, the Object Itself Is Not
Composite’s constructor throws an exception after constructing two SubPart
members. Are the already-constructed members destroyed? Does Composite’s own
destructor ever run?
// exception_interrupted_construction.cpp — the already-constructed parts are destroyed, the object itself is never destroyed
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
class SubPart {
public:
explicit SubPart(std::string name) : name_(std::move(name)) {
std::cout << name_ << " constructed\n";
}
~SubPart() {
std::cout << name_ << " destroyed\n";
}
private:
std::string name_;
};
class Composite {
public:
Composite() : first_("first"), second_("second") {
std::cout << "Composite body running, throwing exception\n";
throw std::runtime_error("construction error");
}
~Composite() {
std::cout << "Composite destructor ran\n";
}
private:
SubPart first_;
SubPart second_;
};
int main() {
try {
Composite c;
} catch (const std::exception& e) {
std::cout << "caught error: " << e.what() << "\n";
}
return 0;
}
first constructed second constructed Composite body running, throwing exception second destroyed first destroyed caught error: construction error
PS14. first_ and second_ are constructed in the member initializer list. The
exception thrown in the body cuts the constructor short: Composite never
completes. As the stack unwinds, the members constructed up to that point are
destroyed in reverse order — first second_, then first_ — but “Composite
destructor ran” is never printed: the destructor is only called if the constructor has
completed. Class copy does not appear directly here, but catch (const std::exception& e) catches by reference and produces no extra copy.
A Temporary Object Is Copied Fewer Times Than a Named Local
In PS13, throwing a named local and catching it by value gave a copy count of 2.
Does that number change if a temporary is thrown directly (throw ErrorObject();) and
still caught by value?
// exception_temporary_copy.cpp — throwing a temporary object copies fewer times than a named local
#include <iostream>
class ErrorObject {
public:
ErrorObject() = default;
ErrorObject(const ErrorObject&) { ++copy_count; }
static int copy_count;
};
int ErrorObject::copy_count = 0;
void throw_temporary() {
throw ErrorObject();
}
int main() {
try {
throw_temporary();
} catch (ErrorObject e) {
(void)e;
}
std::cout << "copy count when throwing a temporary (catch by value) "
<< ErrorObject::copy_count << "\n";
return 0;
}
copy count when throwing a temporary (catch by value) 1
PS15. throw_temporary does not construct a named local; it constructs and throws
a temporary (prvalue) directly. In C++17, an exception object initialized from a
temporary (prvalue) is constructed directly in the exception storage without calling
the copy constructor, so the first copy drops out. The only remaining copy is the one
from catching by value: total 1, one less than PS13’s 2. Class copy:
whether the thrown expression is a named local or a temporary changes the copy count.
Stack Unwinding Spans Multiple Frames: Each Layer Destroys Its Own Locals
PS14 measured a construction interrupted inside a single constructor. If an exception is thrown at the innermost of several nested function calls, are the local objects in the intermediate frames also destroyed by the same stack-unwinding mechanism, and in what order?
// exception_stack_unwinding.cpp — in multi-frame stack unwinding, each frame destroys its own locals in reverse order
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
class Tracker {
public:
explicit Tracker(std::string name) : name_(std::move(name)) {
std::cout << name_ << " constructed\n";
}
~Tracker() {
std::cout << name_ << " destroyed (during stack unwinding)\n";
}
private:
std::string name_;
};
void third_level() {
Tracker t3("third level");
throw std::runtime_error("error deep inside");
}
void second_level() {
Tracker t2("second level");
third_level();
}
void first_level() {
Tracker t1("first level");
second_level();
}
int main() {
try {
first_level();
} catch (const std::exception& e) {
std::cout << "caught error: " << e.what() << "\n";
}
return 0;
}
first level constructed second level constructed third level constructed third level destroyed (during stack unwinding) second level destroyed (during stack unwinding) first level destroyed (during stack unwinding) caught error: error deep inside
PS35. The locals of the three frames are printed in construction order; destruction
happens in reverse: first t3, then t2, then t1. Stack unwinding destroys the
locals of every frame it passes through until it reaches the catch — the rule from
PS14’s single constructor, generalized to the entire call stack. Class copy does
not appear, but the mechanism is the same.
throw; Preserves the Original Object, throw e; Slims It to the Static Type
There are two ways to rethrow a caught exception: the parameterless throw; and
throw e;, which throws the caught variable. Do they carry the same exception out?
// exception_throw_vs_throw_e.cpp — throw; preserves the original type, throw e; slims it to the parameter's static type
#include <iostream>
#include <stdexcept>
class BaseError : public std::runtime_error {
public:
BaseError() : std::runtime_error("base error") {}
};
class DerivedError : public BaseError {
public:
DerivedError() = default;
};
void rethrow_original() {
try {
throw DerivedError();
} catch (BaseError&) {
throw; // rethrows the caught object as is, with its dynamic type
}
}
void rethrow_slimmed() {
try {
throw DerivedError();
} catch (BaseError& e) {
throw e; // constructs a new object from e's static type (BaseError)
}
}
int main() {
try {
rethrow_original();
} catch (DerivedError&) {
std::cout << "after throw;, caught as DerivedError\n";
} catch (BaseError&) {
std::cout << "after throw;, caught as BaseError\n";
}
try {
rethrow_slimmed();
} catch (DerivedError&) {
std::cout << "after throw e;, caught as DerivedError\n";
} catch (BaseError&) {
std::cout << "after throw e;, caught as BaseError\n";
}
return 0;
}
after throw;, caught as DerivedError after throw e;, caught as BaseError
PS36. throw; rethrows the caught object as is; the outer catch still catches it
as DerivedError — the dynamic type is preserved. throw e; instead constructs a
new object from e’s static type (BaseError); catch (DerivedError&) no longer
matches — the same root as the object slicing in PS21. Class copy: throw e;
carries a copy constructed from e‘s static type, not the original’s.
Catching the Base Class by Value Slices the Derived Exception
DerivedError overrides the virtual function type_name(). When the same object is
caught once by value and once by reference as BaseError, which version does
type_name() reach?
// exception_catch_by_value_slicing.cpp — a derived exception caught by value into the base is sliced, caught by reference is not
#include <iostream>
#include <stdexcept>
class BaseError : public std::exception {
public:
virtual const char* type_name() const { return "BaseError"; }
};
class DerivedError : public BaseError {
public:
const char* type_name() const override { return "DerivedError"; }
};
void throw_derived() {
throw DerivedError();
}
int main() {
try {
throw_derived();
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcatch-value="
} catch (BaseError e) { // the compiler deliberately warns about catching by value here, the measurement shows exactly this behavior
#pragma GCC diagnostic pop
std::cout << "type_name() of the object caught by value: " << e.type_name() << "\n";
}
try {
throw_derived();
} catch (const BaseError& e) {
std::cout << "type_name() of the object caught by reference: " << e.type_name() << "\n";
}
return 0;
}
type_name() of the object caught by value: BaseError type_name() of the object caught by reference: DerivedError
PS37. catch (BaseError e) catches by value: the object is copied into a new
object of type BaseError — slicing similar to PS21, type_name() reaches
BaseError. catch (const BaseError& e) binds directly to the stored
DerivedError, does not slice: DerivedError. This is why exception hierarchies
are almost always caught by reference. Class copy: the object produced by
catch (BaseError e) is a copy of only the original’s BaseError part.
Three Levels of Exception Guarantee: The Strong Level Preserves State, the Basic Level May Change It
After a function that throws an exception, an object’s state can be in one of three levels: the strong guarantee (returns to the state before the call), the basic guarantee (valid but may have changed), and the no-throw guarantee (the next measurement). Two addition functions that cross the same limit — one written with the strong guarantee, one with the basic guarantee — how do they differ after a failed call?
// exception_guarantee_levels.cpp — the strong guarantee does not change state after a failed call, the basic guarantee may
#include <iostream>
#include <stdexcept>
class Counter {
public:
void strong_add(int amount) {
int new_value = value_ + amount; // computed in a local variable, value_ has not changed yet
if (new_value > 100) {
throw std::runtime_error("limit exceeded");
}
value_ = new_value; // assignment happens only once success is certain
}
void basic_add(int amount) {
value_ += amount / 2; // the first half is applied directly first
if (value_ + amount / 2 > 100) {
throw std::runtime_error("limit exceeded");
}
value_ += amount / 2; // then the remaining half
}
int value() const { return value_; }
private:
int value_ = 90;
};
int main() {
Counter c1;
try {
c1.strong_add(20);
} catch (const std::exception&) {
std::cout << std::boolalpha
<< "strong_add failed, did the value change: "
<< (c1.value() != 90) << "\n";
}
Counter c2;
try {
c2.basic_add(20);
} catch (const std::exception&) {
std::cout << std::boolalpha
<< "basic_add failed, did the value change: "
<< (c2.value() != 90) << "\n";
}
return 0;
}
strong_add failed, did the value change: false basic_add failed, did the value change: true
PS38. strong_add performs the limit check in a local variable and only
assigns to value_ once success is certain; value_ is never touched: false —
the strong guarantee. basic_add applies the limit in two steps, wedging the check
between them; at the moment it throws, value_ has already partly changed: true —
the basic guarantee. The third level is covered in the next measurement via
noexcept. Class itself: the difference is not in the object’s identity, but in
the state the call leaves it in.
Copy, Swap, Swap-Exchange: Earning the Strong Guarantee From Structure
Instead of writing the assignment operator by hand, taking the parameter by value
and swapping it with an exchange function is another well-known pattern that provides
the strong guarantee. Does it produce the correct result in a normal assignment and in
self-assignment (c = c;)?
// exception_copy_swap_exchange.cpp — the copy-swap-exchange pattern gives the correct result in normal assignment and self-assignment
#include <iostream>
#include <string>
#include <utility>
class Buffer {
public:
explicit Buffer(std::string data) : data_(std::move(data)) {}
Buffer(const Buffer& other) : data_(other.data_) {}
friend void exchange(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
}
Buffer& operator=(Buffer other) { // taken by value: the copy is made at the call itself
exchange(*this, other);
return *this;
} // other is destroyed here, taking the old data_ with it
const std::string& data() const { return data_; }
private:
std::string data_;
};
int main() {
Buffer a("first");
Buffer b("second");
a = b;
std::cout << "after assignment a: " << a.data() << ", b: " << b.data() << "\n";
Buffer c("third");
c = c;
std::cout << "after self-assignment c: " << c.data() << "\n";
return 0;
}
after assignment a: second, b: second after self-assignment c: third
PS39. operator=(Buffer other) takes the parameter by value; other is
constructed as an independent copy at call time — the copy completes before the
body runs, the only place that can throw. The body only calls the noexcept
exchange: this ordering gives the strong guarantee automatically — if the copy fails,
*this is never changed. In self-assignment (c = c;), no special check is needed;
since other is an independent copy, the result is still third. Class copy:
the guarantee rests on making a separate copy before the assignment.
noexcept: A No-Throw Contract and the Move Choice on Reallocation
When std::vector exceeds its capacity, it reallocates and either moves or copies the
old elements. The standard requires copying if the move constructor is not
noexcept — an exception thrown mid-move during reallocation could leave elements
unrecoverable. Do two classes, one with a noexcept move constructor and one without,
really behave differently when grown with emplace_back?
// exception_noexcept_vector_choice.cpp — a noexcept-marked move constructor takes precedence over copying during vector reallocation
#include <iostream>
#include <vector>
class SafeMove {
public:
SafeMove() = default;
SafeMove(const SafeMove&) { ++copy_count; }
SafeMove(SafeMove&&) noexcept { ++move_count; }
static int copy_count;
static int move_count;
};
int SafeMove::copy_count = 0;
int SafeMove::move_count = 0;
class UnsafeMove {
public:
UnsafeMove() = default;
UnsafeMove(const UnsafeMove&) { ++copy_count; }
UnsafeMove(UnsafeMove&&) { ++move_count; } // not noexcept
static int copy_count;
static int move_count;
};
int UnsafeMove::copy_count = 0;
int UnsafeMove::move_count = 0;
int main() {
{
std::vector<SafeMove> v;
v.reserve(1);
for (int i = 0; i < 5; ++i) {
v.emplace_back();
}
}
std::cout << std::boolalpha
<< "did reallocation with a noexcept move constructor use any copies: "
<< (SafeMove::copy_count > 0) << "\n";
std::cout << "did reallocation with a noexcept move constructor use moves: "
<< (SafeMove::move_count > 0) << "\n";
{
std::vector<UnsafeMove> v;
v.reserve(1);
for (int i = 0; i < 5; ++i) {
v.emplace_back();
}
}
std::cout << "did reallocation with a non-noexcept move constructor use any copies: "
<< (UnsafeMove::copy_count > 0) << "\n";
return 0;
}
did reallocation with a noexcept move constructor use any copies: false did reallocation with a noexcept move constructor use moves: true did reallocation with a non-noexcept move constructor use any copies: true
PS40. SafeMove’s move constructor is noexcept: vector never uses copies while
reallocating — copy_count > 0 is false, move_count > 0 is true.
UnsafeMove’s is not noexcept: vector chooses to copy — copy_count > 0 is
true. noexcept is not just a documentation note; the standard library runs
different code based on this contract; breaking the promise and throwing triggers
std::terminate, and that path was not run here. Class copy: the presence of
noexcept determines whether elements are moved with a copy or a move.
The Bounding Measurement
In this lesson, exceptions were always caught with a single or a sequence of catch
blocks; catching every type with catch (...) and partial matching within the same
hierarchy were out of scope. That a noexcept-marked function actually throwing
triggers std::terminate was not measured — since the program could not end cleanly,
it cannot be shown in the sandbox; only the case where the contract is kept was
measured. The copy-swap-exchange pattern’s core guarantee — that if the copy
constructor itself throws, *this remains untouched — was only addressed
indirectly here; the case where the copy constructor actually throws was not run
separately.
Summary
throw e;copies a named local variable into the exception storage once;catch (ErrorObject e)catches this stored object by value, producing a second copy.catch (const ErrorObject&)binds directly to the stored object, producing no extra copy.- An exception thrown in a constructor’s body destroys the members constructed up to that point in reverse construction order, but never calls the object’s own destructor — because the constructor did not complete.
- Throwing a temporary object directly (
throw ErrorObject();) produces one fewer copy than throwing a named local. - The expression this lesson measures falls in the copy class: how an exception is thrown and caught directly determines how many copies end up being made.
Next Step
In this lesson, ErrorObject::copy_count shared the same counter across every call as
a static member. The next lesson, Namespaces and Code Organization, measures how two
symbols with the same name are separated by namespaces, and whether static/extern
linkage confines a symbol to a single file or shares it across files — again the
question of which symbol an expression works with, but now across file boundaries
rather than class boundaries.
To keep your progress and take notes, Log in
My notes
Log in to take notes.