Debugging — 8 seeded hunts
← Module home · Each snippet compiles (or nearly) but misbehaves. Predict the symptom, form a hypothesis, then read the separated diagnosis. Several diagnoses involve reading template errors — Lesson 2 §4’s skill, exercised.
D1 — The sum that evaporated
#include <iostream>
using namespace std;
class Money {
public:
Money(long long p = 0) : paisa(p) {}
long long getPaisa() const { return paisa; }
private:
long long paisa;
};
Money& operator+(const Money& a, const Money& b) {
Money result(a.getPaisa() + b.getPaisa());
return result;
}
int main() {
Money m = Money(100) + Money(250);
cout << m.getPaisa() << "\n"; // expected 350
return 0;
}
The compiler warned. What dangles, when does the symptom appear, and which two signature families are honest?
Diagnosis
operator+ returns a reference to the local result, which dies at the closing brace — the caller receives a reference to a destroyed object; reading getPaisa() is undefined behaviour (350, garbage, anything). The warning: “reference to local variable ‘result’ returned.” Honest families: binary arithmetic returns by value (Money operator+); reference returns exist only for *this (the += family) and the stream in <</>> — both of which outlive the call. (Gallery #2 of Lesson 1 — the hunt’s whole payload.)
D2 — The asymmetric equality
class Temperature {
public:
Temperature(double c) : celsius(c) {} // note: NOT explicit
bool operator==(const Temperature& o) const { return celsius == o.celsius; }
private:
double celsius;
};
int main() {
Temperature t(36.6);
if (t == Temperature(36.6)) cout << "A"; // prints A
if (t == 36.6) cout << "B"; // prints B (conversion serves)
if (36.6 == t) cout << "C"; // ← fails to compile?
return 0;
}
Explain exactly why C fails while B works, name the structural cause, and give the two honest cures.
Diagnosis
B compiles because the right operand converts: the non-explicit constructor turns 36.6 into Temperature for the member operator==. C cannot compile: for a member operator the left operand must be the class — a conversion may apply to the right side only, and 36.6 has no member functions to call. Structural cause: members fix their left operand (Lesson 1 §2’s symmetry argument). Cures: (1) move == to a non-member taking (const Temperature&, const Temperature&) — then the conversion serves both sides; (2) keep explicit and add the deliberate non-member pair ==(const Temperature&, double) / ==(double, const Temperature&). The design decision (explicit or converting) and the comparison set go together — E10’s rule, now with the failing line in hand.
D3 — The chain that stopped
#include <iostream>
using namespace std;
class Die {
public:
friend ostream& operator<<(ostream& out, const Die& d);
private:
int face = 4;
};
ostream& operator<<(ostream& out, const Die& d) {
out << "die shows " << d.face;
}
int main() {
Die d;
cout << d << "\n"; // expected: die shows 4, then a newline
return 0;
}
The first run printed the face. The newline never came — and on some compilers this doesn’t compile at all. Diagnose both behaviours and the four-word rule.
Diagnosis
operator<< forgot return out; — it returns “nothing”, which is a compile error outright (“return-statement with no value in a function returning ‘ostream&’”). The versions that “printed the face then lost the newline” are the even sneakier sibling: a return; (void return) that some compilers accept in a warning-only mode — the chain (cout << d) << "\n" then calls << on a destroyed stream reference. Four-word rule: operator<< returns the stream. (Gallery #4; the OOP module’s chaining requirement, now the debugging hunt.)
D4 — The contract that wasn’t
#include <iostream>
#include <string>
using namespace std;
template <typename T>
const T& myMax(const T& a, const T& b) {
return (a < b) ? b : a;
}
class Wallet {
public:
Wallet(long long p = 0) : paisa(p) {}
long long getPaisa() const { return paisa; }
private:
long long paisa;
};
int main() {
Wallet w1(100), w2(250);
cout << myMax(w1, w2).getPaisa() << "\n"; // expected 250
return 0;
}
The template itself is fine. The stamp fails — name the error’s shape, locate the one line of your code inside the wall of instantiation noise, and give the fix (both the operator and the contract comment).
Diagnosis
The error is a deduction-stage failure: the compiler tries to stamp myMax<Wallet> and inside the pattern body finds a < b with no operator< for Wallet — the message (“no match for ‘operator<’”) arrives buried under template-instantiation context lines. The skill: the first mention of your own file/line (mymax.cpp:19 or wherever the call is) is the real location; the rest is the stamping trail. Fix: give Wallet a bool operator<(const Wallet& o) const { return paisa < o.paisa; } — and write the template’s contract comment (“T requires <“) so the next user meets the requirement before the compiler does. (Lesson 2 §4’s limitation 1 and mistake 1, exercised together.)
D5 — The deduction that couldn’t agree
template <typename T>
const T& myMin(const T& a, const T& b) {
return (a < b) ? a : b;
}
int main() {
cout << myMin(3, 4.5) << "\n"; // the line
return 0;
}
Why exactly does deduction fail here (name the two competing deductions), and what are the three cures — including the honest one-paragraph answer for which cure preserves the no-copy property?
Diagnosis
Arg 1 deduces T = int; arg 2 deduces T = double — one parameter, two contradictory demands, deduction fails (“no matching function… deduced conflicting types”). Cures: (1) matching literals — myMin(3.0, 4.5); (2) explicit T — myMin<double>(3, 4.5) (the int converts); (3) the two-parameter template template <typename A, typename B> A myMin2(const A&, const B&) — returning A, the first argument’s type, the caller’s chosen common currency. The no-copy note: all three preserve const& parameters — nothing is copied in; the explicit-T and two-parameter forms may convert one argument into a temporary (which the const& legally binds) — the honest answer names that temporary and its lifetime (the full expression, like any temporary).
D6 — The Box that leaked
template <typename T>
class Box {
public:
Box(int cap) : cap(cap > 0 ? cap : 1) { data = new T[cap]; }
~Box() { delete[] data; }
bool add(const T& item) { if (count >= cap) return false; data[count++] = item; return true; }
private:
T* data;
int cap, count = 0;
};
int main() {
Box<int> a(5);
Box<int> b = a; // ← the line
b.add(9);
return 0;
}
This compiles, runs, and corrupts. Name the event, trace the two objects’ lifelines, and connect the hunt to the Inheritance module’s D4 — then state the two honest postures for this course level.
Diagnosis
Box<int> b = a; copies the Box by value — the compiler-generated copy copies the pointer: two Boxes, one heap block. b.add(9) writes through the shared block (a corrupts); both destructors delete[] it (double-free, undefined behaviour). Same hunt as the Inheritance module’s D4 (SessionLog by value) — generalized: every resource-owning template poisons its copies. Honest postures at this level: (1) pass Boxes by reference/const& and never copy them (the ownership comment at the class head); (2) the proper machinery — user-defined copy constructor/assignment (the “rule of three”), named here, built next course.
D7 — The template in the wrong file
// mymath.cpp
#include "mymath.hpp"
template <typename T>
T clampTo(T v, T lo, T hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
// (definition lives ONLY here; the header declares nothing)
// main.cpp
#include "mymath.hpp"
int main() {
// int x = clampTo(15, 0, 10); // ← the line, currently commented
return 0;
}
The program links fine today. Uncomment the line in main.cpp and describe the exact failure, its cause (the §2 scope note), and the layout that fixes it.
Diagnosis
Linker error: “undefined reference to int clampTo<int>(int, int, int)” — not a compile error. Cause: templates are stamped where used, so the compiler must see the definition in every translation unit that stamps it; main.cpp saw only nothing (the header declared nothing), so it emitted a call to a function no file ever generated. Fix: template definitions live in the header (mymath.hpp) — the §2 scope note’s rule, now felt. (Secondary fix: the header needs some declaration for the definition to make sense; in practice the whole template — template line and body — moves into the header.)
D8 — The audit: three bugs, one file
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class Score {
public:
Score(const T& value) : value(value) {}
bool operator<(const Score& o) { return value < o.value; } // line A
friend ostream& operator<<(ostream& out, const Score& s) {
out << s.value;
} // line B
private:
T value;
};
template <typename T>
T best(const Score<T> arr[], int n) {
Score<T> champion(0); // line C
for (int i = 0; i < n; i++)
if (arr[i] < champion) champion = arr[i];
return champion; // line D
}
int main() {
Score<int> scores[3] = { Score<int>(50), Score<int>(90), Score<int>(70) };
cout << best(scores, 3) << "\n"; // expected 90
return 0;
}
Find all three defects before reading the list — classify each: comparison-logic bug, stream bug, or generic-design bug.
Diagnosis
Line A — comparison-logic bug, twice over: operator< is not const, so it cannot be called where a const Score appears (a const Score<T>& parameter, a const array element — the compiled build only survives because every object here is mutable); and the loop direction tracks the minimum, not the best — it keeps arr[i] when arr[i] < champion, so expected 90 prints 50. Fixes: bool operator<(const Score& o) const and flip the comparison (champion < arr[i]). Line B — stream bug: the friend operator<< returns nothing (the D3 hunt again) — add return out;. Line C/D — generic-design bug: Score<T> champion(0) assumes T constructible from 0 — a requirement the template never stated, and one string fails outright; returning T (line D) also copies. Honest design: seed from the first element (if (n <= 0) throw; Score<T> champion = arr[0]; for (int i = 1; ...) if (champion < arr[i]) champion = arr[i];) and state the contract in a comment (“T: copyable, <, <<“) — the seed-from-first-element pattern the Algorithms module taught, now in generic dress. Verdict order: logic → stream → design.
Fix-list recap
| Hunt | Bug class | Prevention rule |
|---|---|---|
| D1 | reference to local | + by value; & only for *this/streams |
| D2 | member asymmetry | comparisons are non-members (or pairs) |
| D3 | stream returns nothing | operator<< returns the stream |
| D4 | unstated contract | comment the requirements; read your line in the wall |
| D5 | deduction conflict | match literals / explicit T / two-parameter form |
| D6 | copied resource owner | never copy boxes; rule-of-three next course |
| D7 | template in a .cpp | definitions live in headers |
| D8 | three-for-one audit | logic → stream → design; seed from the first element |
Where next
- Design problems: six operator/template judgements.
- The five labs: build them properly.
— Engr. Dr. Muhammad Nadeem Majeed, Professor, Department of Data Science (PUCIT), University of the Punjab, Lahore · About the instructor