Skip to content

Debugging — Answers

Back to the questions

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:

error: passing 'const Sensor' as 'this' argument discards qualifiers [-fpermissive]

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:

class Base { public: virtual ~Base() { std::cout << "~Base\n"; } };

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:

free(): double free detected in tcache 2
Aborted (core dumped)

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 (auto it = v.begin(); it != v.end(); ) {
    if (*it % 2 == 0) it = v.erase(it);
    else              ++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:

error: 'class std::runtime_error' has no member named 'field'

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 (const ValidationError& e) { e.field(); }

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:

terminate called after throwing an instance of 'std::runtime_error'
  what():  cannot close

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 <:

bool operator<(const Item& o) const { return v_ < o.v_; }

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:

std::unique_ptr<Report> make() { return std::make_unique<Summary>(); }

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.