Debugging — Answers¶
For each defect: the name, when it bites, and the fix.
D1 — a missing const.
celsius() is not a const member function, so calling it on s, which is a
const Sensor&, is a compile error:
It bites immediately, at compile time — this is why it is worth listing first: the output
of a missing const is a diagnostic, not a crash. Fix: double celsius() const { return celsius_; }.
A good rule: if a member function does not modify the object, make it const as a matter of
course, not when someone complains.
D2 — a missing virtual.
Base::speak is not virtual, so the call goes to Base::speak and prints Base. It bites
at run time, silently — no warning, no crash, just the wrong function. Fix: declare it
virtual void speak() const in Base and mark the derived one override:
class Base { public: virtual void speak() const { std::cout << "Base\n"; } };
class Derived : public Base { public: void speak() const override { std::cout << "Derived\n"; } };
Since the base has a virtual function and is used as a base, it should also have a virtual destructor.
D3 — a non-virtual destructor, deleted through the base.
delete p runs only Base::~Base (or worse, undefined behaviour, because the objects may
have different layouts). The derived part is never destroyed, so anything it owned leaks.
It bites at run time, silently leaking. Fix:
A simple test: if a class has any virtual function, its destructor must be virtual too.
D4 — a shallow copy: a double free.
The compiler-generated copy constructor copies the int*, so a.data_ and b.data_ point
at the same array. Both destructors delete[] it, so the second is a double free:
It bites at the end of main, when both destructors run. Fix: the Rule of Three — add a
copy constructor and a copy assignment operator that allocate their own array and copy the
elements (and keep the destructor).
D5 — self-assignment.
If someone writes b = b, delete[] data_ frees the buffer, and the loop then reads
other.data_ — which is the same now-dangling pointer. The result is a use-after-free or
garbage. It bites whenever a caller assigns an object to itself, which is legal and
sometimes happens indirectly through references. Fix: guard it, then do the work — the
safest version takes the copy before freeing:
Blob& operator=(const Blob& other) {
if (this == &other) return *this; // self-assignment guard
int* fresh = new int[other.n_];
for (int i = 0; i < other.n_; ++i) fresh[i] = other.data_[i];
delete[] data_;
data_ = fresh;
n_ = other.n_;
return *this;
}
The copy-and-swap idiom (Blob(std::move(other)).swap(*this)) gets the same safety for
free.
D6 — name hiding.
Derived::show(int) hides all Base::show overloads, so the d.show() call finds no
zero-argument candidate:
error: no matching function for call to 'Derived::show()'
note: candidate: 'void Derived::show(int) const'
It bites at compile time. Fix, if the overloads are meant to coexist:
struct Derived : Base {
using Base::show; // bring the base overloads back into scope
void show(int) const {}
};
Otherwise call it explicitly: d.Base::show();.
D7 — erasing while iterating.
v.erase(it) invalidates it, and the loop then does ++it on an invalidated iterator —
undefined behaviour, which usually skips an element (here the 4 or nothing, depending on
the implementation). It bites at run time, unpredictably. Fix — erase returns an iterator
to the next element, so use it:
For a whole-container rule, std::remove_if + erase is clearer and harder to get wrong.
D8 — catching by value, and the wrong type.
Two defects at once. catch (std::runtime_error e) catches by value, which slices the
ValidationError down to its std::runtime_error part, so e has no field() — the line
does not even compile:
It bites at compile time here; had the body not called field(), the slice would still be
silently wrong for any derived type. Fix: catch the derived type by const reference:
Catch by const reference, and catch the most-derived type you intend to handle.
D9 — a destructor that throws.
work() throws, unwinding begins, and Guard::~Guard throws a second exception while one
is already in flight. C++ cannot have two active exceptions, so the runtime calls
std::terminate — the program dies immediately:
It bites the first time an exception path meets a throwing destructor. Fix: a destructor
must never throw (noexcept by default). Do the operation that can fail in an explicit,
callable close()/commit() method, and have the destructor only do work that cannot fail.
D10 — a dangling pointer.
checkIn stores the address of its parameter, and in session that parameter is bound
to the local temp, which is destroyed when session returns. The room now holds a pointer
to dead stack memory; any later read is undefined behaviour. It bites after the lifetime
ends, silently until it is not. Fix: either store the value (std::vector<int>), or make
the caller's lifetime guarantee explicit — take the object by reference and document that it
must outlive the room (void checkIn(const Element& e) { present_.push_back(&e); }).
D11 — a broken strict weak ordering.
operator< returns v_ <= o.v_, so a < a is true. std::sort requires strict weak
ordering; with an irreflexive comparison violated the algorithm can loop or write out of
bounds. It bites at run time — often a crash or corrupted input — and only with certain
inputs. Fix: use <:
This is also what makes std::map/std::set keys work.
D12 — returning a pointer to a local.
make() returns &s, but s is a local that dies when make returns, so the caller holds
a dangling pointer. If they delete it — plausible, given the Report* — that is undefined
behaviour as well. It bites on any use after the return. Fix: return ownership, not an
address:
Or return by value. Never return the address of a local (or a reference to one).
Scoring. Twelve questions, three parts each (name, when, fix). One point per part, so 36 points; most students find one defect but miss the second in D4 and D5. See Marking Yourself.