Chapter 24 — Inheritance: Report-Logger Family
You'll build a small report-logger family — a base class Logger and two
derived classes — using non-virtual public inheritance. The family models a
real pattern: a base class provides core behavior (formatting, counting) and
derived classes specialize it. Every key Chapter 24 concept is observable in
the output, not just readable in the code.
The exercise ends with a cliffhanger: a free function processLog accepts a
Logger& and calls log(). When you pass a TimestampLogger through that
reference, the base Logger::log() runs — even though the object is a
TimestampLogger. The tests assert this "expected-but-disappointing" behavior
on purpose, and the last line of this README explains why it exists. This is the
problem Chapter 25 — virtual functions — was built to solve.
The class hierarchy mirrors LLVM's own Instruction family: a base class owns
common behavior; derived classes specialize it; helper functions accept base
references and (pre-Chapter 25) only reach the base behavior. You will read code
exactly like this in every CS6340 assignment. (notes 24.1)
Your tasks
Logger constructor (TASK 1). Initialize all data members in the member-initializer list (
m_lineCount,m_dtorTrace,m_name,m_log). In the constructor body, append"Logger+"toctorTrace. This is the observable proof that the base constructs first (notes 24.3).Logger::log(msg)(TASK 2). Return the formatted string"[" + m_name + "] " + msg. Incrementm_lineCountand appendentry + "\n"tom_log. Return the entry. This function is called byTimestampLogger::log()via the explicitLogger::log(msg)syntax and also inherited unchanged byCountingLogger.TimestampLoggerconstructor (TASK 3). In the member-initializer list, callLogger { name, ctorTrace, dtorTrace }(the ONLY way to initialize the base subobject — notes 24.4) and initializem_tag { tag }. Append"Timestamp+"in the body. Full trace becomes"Logger+Timestamp+".TimestampLogger::log(msg)(TASK 4). Prepend the tag: builddecorated = m_tag + " " + msg. Then callLogger::log(decorated)— the explicit base-call syntax is required; plainlog(decorated)would recurse forever (notes 24.7). Return the result from the base.CountingLogger::linesLogged()(TASK 5). Returnm_lineCount— the protected base-class member. This compiles becauseCountingLoggeris a derived class; public users cannot accessm_lineCountdirectly (notes 24.5). (TheCountingLoggerconstructor body is provided, already correct.)processLog(l, msg)— the cliffhanger (TASK 6). A one-liner:return l.log(msg);. The test intentionally passes aTimestampLoggerthroughLogger& land asserts the base format — no tag — because static binding resolves the call usingl's declared type (Logger), not the runtime type of the object. This is the correct, expected, but limited result.
Success criteria
ct.substr(0, 7) == "Logger+"— base token appears before derived token in the construction trace (notes 24.3)ct == "Logger+Timestamp+"— both tokens present, correct orderdt == "Timestamp-Logger-"— destruction reverses construction order (notes 24.3)tl.log("connect") == "[audit] [T] connect"— derivedlog()prepends tag then calls base; tag AND base format both present (notes 24.7)cl.linesLogged() == 2after two calls — protectedm_lineCountreadable from derived (notes 24.5)processLog(tl, "via_ref") == "[router] via_ref"— NO tag — static binding runsLogger::log()even when the object is aTimestampLogger(notes 24.7; this is the cliffhanger — correct and intentional)
Concepts practiced
New in Chapter 24:
- Public inheritance (
: public Base) — the "is-a" relationship (notes 24.2) - Base / derived construction order — base constructs first; destruction reverses (notes 24.3); made observable through trace strings
- Derived constructors — calling the immediate base constructor in the member-initializer list; cannot directly initialize private base members (notes 24.4)
protectedaccess —m_lineCountis protected soCountingLoggercan read it directly; public users cannot (notes 24.5)- Adding new functionality —
CountingLoggeraddslinesLogged()without redefininglog()(notes 24.6) - Function redefinition (hiding) —
TimestampLoggerredefineslog(), hiding the base version for calls on aTimestampLoggerobject (notes 24.7) - Calling the base version explicitly —
Logger::log(msg)insideTimestampLogger::log()(notes 24.7) - Static binding's limits — a call through
Logger&always resolves toLogger::log()regardless of the runtime object type (notes 24.7 CS6340 tie-in)
Reused from earlier chapters:
class, constructors, member-initializer lists, access specifiers (Ch 14)constmember functions (Ch 14–15)std::stringand+=concatenation (Ch 5)- Reference parameters and returning by value (Ch 12)
- Header guards and the
.h/.cppsplit (Ch 2)
Constraints
- Allowed: everything through Chapter 23; public inheritance (
: public Base);protectedmembers; callingBase::fn()explicitly; adding new members to derived classes;std::stringand+=/+. - Forbidden (not taught yet — notes 24 scope):
virtual/override/final(Chapter 25 — using them would break the cliffhanger test by design),dynamic_cast(Chapter 25), multiple inheritance in your implementation (the README mentions it in notes 24.9 but the exercise avoids it to keep scope clean). - Required idioms:
- Call the base constructor in the member-initializer list, not in the body.
- Use
Logger::log(msg)(notlog(msg)) when calling the base from a derived function — this is the explicit-base-call syntax from notes 24.7. - Do not bypass the base constructor by initializing
Logger's private members directly from a derived class (notes 24.4 — it would not compile).
Build & run locally
make # compile-check starter/logger.cpp -> OK or error
make test # grade your code -> RED until the TASK blocks are filled in
make solution # build + run the reference demo
make test-solution # proof the solution is green (used by the author)
make run # run the starter demo (fun once your code works)
make clean # remove build artifactsThe grader (tests/tests.cpp) compiles starter/logger.cpp together with the
test file and runs assertions on the API. No main() in your .cpp needed.
Hints
Task 1 — member-initializer list syntax and order
Members are initialized in the order they are declared in the class, not the
order they appear in the list. The Logger header declares:
m_lineCount, m_dtorTrace, m_name, m_log (in that order)
A matching initializer list:
Logger::Logger(const std::string& name,
std::string& ctorTrace,
std::string& dtorTrace)
: m_lineCount {}
, m_dtorTrace { dtorTrace }
, m_name { name }
, m_log {}
{
ctorTrace += "Logger+";
}Reference members (m_dtorTrace) must be initialized in the list — you cannot
assign a reference in the body.
Task 2 — build the formatted string
std::string Logger::log(const std::string& msg)
{
std::string entry { "[" + m_name + "] " + msg };
++m_lineCount;
m_log += entry + "\n";
return entry;
}Note the space after "]": the format is "[name] msg", not "[name]msg".
Task 3 — calling the base constructor from the derived initializer list
The base constructor call goes in the derived constructor's initializer list, before the derived member:
TimestampLogger::TimestampLogger(const std::string& name,
const std::string& tag,
std::string& ctorTrace,
std::string& dtorTrace)
: Logger { name, ctorTrace, dtorTrace } // base first
, m_tag { tag } // then derived member
{
ctorTrace += "Timestamp+"; // base already appended "Logger+"
}Logger { name, ctorTrace, dtorTrace } runs the Logger constructor, which
itself appends "Logger+". Then the body appends "Timestamp+", giving
"Logger+Timestamp+" — base before derived (notes 24.3).
Task 4 — why Logger::log() and not log()
std::string TimestampLogger::log(const std::string& msg)
{
std::string decorated { m_tag + " " + msg };
return Logger::log(decorated); // REQUIRED: explicit base scope
}Logger::log(decorated) tells the compiler: "look up log in the Logger
scope, not starting from the current class." Without the Logger:: qualifier,
log(decorated) finds TimestampLogger::log (the current function) and calls
it recursively — an infinite loop. The explicit qualifier is the syntax from
notes 24.7: "use Base::fn() when you mean the inherited function."
Task 5 — reading a protected base member
int CountingLogger::linesLogged() const
{
return m_lineCount; // m_lineCount is protected in Logger
}m_lineCount is declared protected in Logger. That means derived class
bodies (like this one) can read and write it — but public users cannot. If you
wrote cl.m_lineCount in main(), the compiler would reject it. Inside a
member function of CountingLogger, it is accessible (notes 24.5).
Task 6 — the cliffhanger and why the test is correct
std::string processLog(Logger& l, const std::string& msg)
{
return l.log(msg);
}The test passes a TimestampLogger object through Logger& l. Inside this
function, l.log(msg) resolves to Logger::log() — NOT TimestampLogger::log().
This is STATIC BINDING: without virtual, function calls are resolved at compile
time using the declared type of the reference (Logger).
The test:
CHECK_EQ(result, "[router] via_ref"); // NO tag "[T]" — static bindingis intentionally asserting the base behavior. This is correct for a
non-virtual hierarchy. The missing [T] is the observable symptom of static
binding — the "problem" this test is designed to reveal. Chapter 25 fixes it.
Stretch goals
- Change
m_lineCountfrom aprotecteddata member to a private counter with aprotectedaccessor function (int lineCount() const), and updateCountingLogger::linesLogged()to call it. This demonstrates the Chapter 24 note that "protected member functions are often a better compromise than protected data" (notes 24.5). - Add a
using Logger::log;declaration toTimestampLoggerto explore what happens to the base overload set when a derived class introduces its ownlog(notes 24.7, 24.8). - Add
virtualtoLogger::log()(Chapter 25 preview) and watch the cliffhanger test flip: theprocessLogtest will now fail becauseTimestampLogger::log()runs and the tag reappears. That is the fix Chapter 25 exists to provide. - Build a three-level chain: derive
TimestampCountingLoggerfromTimestampLogger, adding both the tag andlinesLogged(). Verify the construction trace becomes"Logger+Timestamp+TSCounting+"— three levels, base-first (notes 24.3, 24.4 immediate-parent rule).
The cliffhanger. processLog accepts a Logger& and calls l.log(msg).
When the object passed is a TimestampLogger, you might expect the timestamp
tag to appear — after all, the object is a TimestampLogger. But without
virtual, the compiler resolves the call using the declared type of the
reference (Logger), not the runtime type of the object. The tag does not
appear. This is STATIC BINDING. It is the correct behavior for a non-virtual
hierarchy, and it is the reason Chapter 25 exists.
// ============================================================================
// starter/logger.cpp — LEARNER IMPLEMENTATION (Chapter 24)
// ----------------------------------------------------------------------------
// Fill in the SIX TASK blocks below. Each maps 1:1 to a task in the README
// and to a declaration in ../logger.h. The bodies currently return
// PLACEHOLDERS so the file compiles immediately — that is why `make test` is
// RED right now. Your job is to turn it GREEN.
//
// make build compile-check your code (should already work)
// make test grade it (RED until you fill these in)
// make solution run the reference demo if you get stuck
// make test-solution verify the solution is green
//
// HOW TO READ THIS FILE: search for ">>> YOUR CODE HERE <<<" to jump to
// each task. Read the surrounding comments carefully — the pedagogy is there.
// ============================================================================
#include "logger.h" // Chapter 2 best practice: include own paired header.
// The -I. Makefile flag makes "logger.h" resolve to the
// chapter root's header. The compiler checks your bodies
// against the contract declared in that header.
// ─── Logger (base class) ─────────────────────────────────────────────────────
// ─── TASK 1: Logger constructor ────────────────────────────────────────────
//
// Initialize the Logger base class:
// - m_lineCount initialize to zero (use the member-initializer list syntax)
// - m_dtorTrace bind to the dtorTrace reference parameter (stored as a
// protected member so derived destructors can append to it)
// - m_name initialize from the `name` parameter
// - m_log default-initialize to an empty string
//
// In the constructor BODY, append "Logger+" to ctorTrace.
// This proves the base constructs before any derived class portion.
// (notes 24.3: "base classes construct first")
//
// SYNTAX REMINDER (notes 24.4):
// Constructor members are initialized in the member-initializer list,
// NOT in the body. The body runs AFTER all members are initialized.
//
// Logger::Logger(const std::string& name, std::string& ctorTrace, std::string& dtorTrace)
// : m_lineCount {} // zero-initialize
// , m_dtorTrace { dtorTrace }
// , m_name { name }
// , m_log {}
// {
// ctorTrace += "Logger+"; // body: append our token
// }
//
Logger::Logger(const std::string& name,
std::string& /*ctorTrace*/,
std::string& dtorTrace)
: m_lineCount {}
, m_dtorTrace { dtorTrace } // must initialize the reference member
, m_name { name } // name stored correctly
, m_log {}
{
// >>> YOUR CODE HERE <<<
// Append "Logger+" to ctorTrace in this body.
// (Hint: ctorTrace += "Logger+";)
}
// ─────────────────────────────────────────────────────────────────────────────
Logger::~Logger()
{
m_dtorTrace += "Logger-"; // PROVIDED: base destructor appends its token
}
const std::string& Logger::name() const
{
return m_name; // PROVIDED: returns the private name
}
// ─── TASK 2: Logger::log(msg) ──────────────────────────────────────────────
//
// Produce a formatted log entry and update internal state:
// 1. Build the formatted string: "[" + m_name + "] " + msg
// 2. Increment m_lineCount.
// 3. Append (entry + "\n") to m_log.
// 4. Return the entry string.
//
// WHY IT MATTERS:
// - TimestampLogger::log() will CALL this version via Logger::log(msg).
// (notes 24.7: "calling the base version explicitly")
// - CountingLogger::linesLogged() reads m_lineCount, which this function
// increments. (notes 24.5: m_lineCount is protected)
//
// >>> YOUR CODE HERE <<<
//
std::string Logger::log(const std::string& /*msg*/)
{
// placeholder — returns the name only; wrong format, count not updated.
return "[" + m_name + "] ";
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TimestampLogger (derived class) ─────────────────────────────────────────
// ─── TASK 3: TimestampLogger constructor ────────────────────────────────────
//
// Initialize the DERIVED class:
// 1. In the member-initializer list, CALL the base constructor:
// Logger { name, ctorTrace, dtorTrace }
// This is the ONLY way to initialize the base subobject. (notes 24.4)
// The base constructor runs FIRST — before m_tag is initialized.
// 2. Initialize m_tag { tag }.
// 3. In the body, append "Timestamp+" to ctorTrace.
// (The base ctor already appended "Logger+", so the final trace is
// "Logger+Timestamp+" — base before derived. notes 24.3)
//
// SYNTAX:
// TimestampLogger::TimestampLogger(...)
// : Logger { name, ctorTrace, dtorTrace } // call base ctor in init list
// , m_tag { tag }
// {
// ctorTrace += "Timestamp+";
// }
//
// >>> YOUR CODE HERE <<<
//
TimestampLogger::TimestampLogger(const std::string& name,
const std::string& /*tag*/,
std::string& ctorTrace,
std::string& dtorTrace)
: Logger { name, ctorTrace, dtorTrace } // call base ctor (required)
, m_tag {} // placeholder: ignores `tag`
{
ctorTrace += "Timestamp+"; // placeholder body — tag ignored
// >>> YOUR CODE HERE <<<
// Replace m_tag {} above with m_tag { tag } to capture the tag parameter.
}
// ─────────────────────────────────────────────────────────────────────────────
TimestampLogger::~TimestampLogger()
{
// TASK 3 (part 2 — PROVIDED): derived destructor appends its token.
// m_dtorTrace is a PROTECTED member in Logger, so this compiles.
// Destruction is REVERSE of construction: this runs BEFORE Logger::~Logger.
// (notes 24.3: "destruction happens in reverse")
m_dtorTrace += "Timestamp-";
}
// ─── TASK 4: TimestampLogger::log(msg) ──────────────────────────────────────
//
// REDEFINE Logger::log() to prefix msg with the tag stored in m_tag.
//
// Step 1: Build a decorated string: m_tag + " " + msg
// Step 2: Call the BASE version: Logger::log(decorated)
// ↑ MUST use Logger:: qualifier — plain log(decorated)
// would recurse into THIS function. (notes 24.7)
// Step 3: Return the result from the base call.
//
// WHY THIS PATTERN (notes 24.7):
// The derived function ADDS behavior (the tag) and DELEGATES the rest to the
// base. This is "calling inherited functions and overriding behavior".
//
// >>> YOUR CODE HERE <<<
//
std::string TimestampLogger::log(const std::string& msg)
{
// placeholder — calls the base directly without the tag.
// (Correct call site, wrong decoration — replace with the real two steps.)
return Logger::log(msg);
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── CountingLogger (derived class) ──────────────────────────────────────────
// ─── TASK 5: CountingLogger constructor + linesLogged() ─────────────────────
//
// Part A (constructor) — CountingLogger adds no new data, so:
// - Call Logger { name, ctorTrace, dtorTrace } in the initializer list.
// - Append "Counting+" in the body. (This part is already provided below.)
//
// Part B (linesLogged) — see the function body after the destructor.
//
CountingLogger::CountingLogger(const std::string& name,
std::string& ctorTrace,
std::string& dtorTrace)
: Logger { name, ctorTrace, dtorTrace } // provided: base call required
{
ctorTrace += "Counting+"; // provided: appends derived token
}
CountingLogger::~CountingLogger()
{
m_dtorTrace += "Counting-"; // PROVIDED
}
// ─── TASK 5 (continued): CountingLogger::linesLogged() ─────────────────────
//
// Return the number of lines logged so far.
// Read the PROTECTED base member m_lineCount directly. (notes 24.5)
//
// WHY THIS WORKS: CountingLogger is a DERIVED CLASS of Logger and m_lineCount
// is declared `protected` in Logger. Derived class code can access protected
// members — that is the whole point of protected. Public users cannot write
// `cl.m_lineCount` from outside the class.
//
// >>> YOUR CODE HERE <<<
//
int CountingLogger::linesLogged() const
{
return 0; // placeholder — always returns zero; replace with m_lineCount
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: processLog (free function — THE CLIFFHANGER) ────────────────────
//
// Accept a Logger BY REFERENCE (Logger& l) and call l.log(msg).
// Return the result.
//
// THIS IS THE MOST IMPORTANT TASK: once you implement it, the test DELIBERATELY
// checks that passing a TimestampLogger through a Logger& runs the BASE log()
// — NOT the derived TimestampLogger::log(). This is STATIC BINDING:
//
// "the compiler resolves the function call at COMPILE TIME using the
// DECLARED type of the reference (Logger), not the runtime type of the
// object (TimestampLogger)." (notes 24.7 CS6340 tie-in)
//
// The tag does NOT appear in the output. This is expected-but-disappointing.
// Chapter 25 solves this with `virtual`.
//
// >>> YOUR CODE HERE <<<
//
std::string processLog(Logger& /*l*/, const std::string& /*msg*/)
{
return {}; // placeholder — returns empty; replace with l.log(msg)
}
// ─────────────────────────────────────────────────────────────────────────────
// ============================================================================
// starter/main.cpp — DEMO DRIVER for the report-logger family (Ch 24)
// ----------------------------------------------------------------------------
// This is an UNGRADED demo that exercises your logger implementation.
// Run it with: make run
//
// DO NOT EDIT THIS FILE — it is provided scaffolding.
// Your work is in starter/logger.cpp (the TASK blocks).
//
// When your TASK blocks are complete, this demo will print the construction
// order, the formatted log entries, and the static-binding cliffhanger.
// ============================================================================
#include <iostream>
#include <string>
#include "logger.h"
int main()
{
std::cout << "============================================================\n";
std::cout << " Chapter 24 — Inheritance: Report-Logger Family Demo\n";
std::cout << "============================================================\n\n";
// ── 1. Basic Logger ───────────────────────────────────────────────────────
std::cout << "── 1. Basic Logger ─────────────────────────────────────────\n";
{
std::string ct, dt;
Logger base { "system", ct, dt };
std::cout << " name(): " << base.name() << "\n";
std::cout << " log(\"boot\"): " << base.log("boot") << "\n";
std::cout << " log(\"running\"): " << base.log("running") << "\n";
}
std::cout << "\n";
// ── 2. Construction order proof (notes 24.3) ──────────────────────────────
std::cout << "── 2. Construction / destruction order (notes 24.3) ────────\n";
{
std::string ct, dt;
{
TimestampLogger tl { "audit", "[T]", ct, dt };
std::cout << " ctor trace: \"" << ct << "\"\n";
}
std::cout << " dtor trace: \"" << dt << "\"\n";
}
std::cout << "\n";
// ── 3. TimestampLogger::log() (notes 24.7) ────────────────────────────────
std::cout << "── 3. TimestampLogger::log() (notes 24.7) ───────────────────\n";
{
std::string ct, dt;
TimestampLogger tl { "router", "[T]", ct, dt };
std::cout << " tl.log(\"connect\") = \"" << tl.log("connect") << "\"\n";
}
std::cout << "\n";
// ── 4. CountingLogger (notes 24.5, 24.6) ─────────────────────────────────
std::cout << "── 4. CountingLogger (notes 24.5 / 24.6) ───────────────────\n";
{
std::string ct, dt;
CountingLogger cl { "counter", ct, dt };
cl.log("startup");
cl.log("shutdown");
std::cout << " linesLogged() = " << cl.linesLogged() << "\n";
}
std::cout << "\n";
// ── 5. THE CLIFFHANGER — static binding (notes 24.7) ──────────────────────
std::cout << "── 5. THE CLIFFHANGER: static binding (notes 24.7) ─────────\n";
{
std::string ct, dt;
TimestampLogger tl { "router", "[T]", ct, dt };
std::cout << " Direct: \"" << tl.log("direct") << "\"\n";
std::cout << " Via Logger& ref: \"" << processLog(tl, "via_ref") << "\"\n";
std::cout << " (no tag in second call — Logger::log() ran, not TimestampLogger::log())\n";
}
std::cout << "\n============================================================\n";
std::cout << " Run `make test` to grade your implementation.\n";
std::cout << "============================================================\n";
return 0;
}
Try the lab first — the learning is in the attempt.
// ============================================================================
// solution/logger.cpp — REFERENCE IMPLEMENTATION (Chapter 24)
// ----------------------------------------------------------------------------
// Complete, correct, warning-clean bodies for the three-class logger family
// declared in ../logger.h. Peek only after you've taken a real swing at
// starter/logger.cpp — the learning is in building the hierarchy yourself.
//
// Every comment here names the NOTES SECTION that motivates the choice so
// you can cross-reference easily.
// ============================================================================
#include "logger.h" // Chapter 2 best practice: include own paired header
// (the -I. flag in the Makefile makes this resolve to
// the chapter root's logger.h regardless of whether
// we're in solution/ or starter/)
// ─── Logger (base class) ─────────────────────────────────────────────────────
// TASK 1: Logger constructor.
//
// MEMBER INITIALIZER LIST (notes 24.4): the comma-separated entries after ':'
// initialize base-class and member data IN DECLARATION ORDER (not the order
// written here). They run BEFORE the constructor body { }.
//
// m_dtorTrace is a PROTECTED REFERENCE MEMBER so derived destructors can
// append their own token without storing a second copy of the reference.
// We initialize it first because members are initialized in declaration order.
//
// The constructor body appends "Logger+" to ctorTrace. This lets the tests
// verify that the base constructs FIRST by checking the trace prefix.
// (notes 24.3: "base classes construct first — the base portion must be
// initialized before the derived portion can safely use it.")
//
Logger::Logger(const std::string& name,
std::string& ctorTrace,
std::string& dtorTrace)
: m_lineCount {}
, m_dtorTrace { dtorTrace }
, m_name { name }
, m_log {}
{
// Append our construction token to the shared trace.
// When constructing a TimestampLogger, this runs BEFORE the
// TimestampLogger constructor body, so the trace starts with "Logger+".
ctorTrace += "Logger+";
}
// Logger destructor.
// Destruction order is REVERSE of construction (notes 24.3):
// derived destructor runs first -> base destructor runs last.
// For a TimestampLogger: trace reads "Timestamp-Logger-" (derived first).
Logger::~Logger()
{
m_dtorTrace += "Logger-";
}
const std::string& Logger::name() const
{
return m_name;
}
// TASK 2: log(msg).
//
// FORMAT: "[<name>] <msg>"
// This function owns the "formatted" token. Derived classes CALL this version
// (via Logger::log) to produce the base format, then decorate it.
//
// INCREMENTS m_lineCount — CountingLogger reads that protected value via
// linesLogged() to report how many messages have been processed. (notes 24.5:
// m_lineCount is protected so derived classes have direct read access.)
//
// Returns the formatted string so callers can check the output directly.
//
std::string Logger::log(const std::string& msg)
{
// Build the formatted entry: "[<name>] <msg>"
std::string entry { "[" + m_name + "] " + msg };
// Update internal state.
++m_lineCount;
m_log += entry + "\n";
return entry;
}
// ─── TimestampLogger (derived class) ─────────────────────────────────────────
// TASK 3: TimestampLogger constructor.
//
// The DERIVED CONSTRUCTOR initializes:
// 1. The BASE PORTION: by calling Logger{ name, ctorTrace, dtorTrace } in the
// initializer list. This is the ONLY way to initialize the base subobject —
// you cannot reach Logger's private members directly. (notes 24.4:
// "derived constructors choose the base constructor")
// 2. Its OWN data: m_tag { tag }.
//
// ORDER (notes 24.3 / 24.4): "Logger{ ... }" in the initializer list runs FIRST.
// Only after the base portion is fully constructed does m_tag get initialized.
// The constructor body then appends "Timestamp+". Because the base constructor
// already appended "Logger+", the final ctorTrace reads "Logger+Timestamp+" —
// the observable proof that base constructs before derived.
//
TimestampLogger::TimestampLogger(const std::string& name,
const std::string& tag,
std::string& ctorTrace,
std::string& dtorTrace)
: Logger { name, ctorTrace, dtorTrace } // BASE portion constructed here
, m_tag { tag } // then our own member
{
// Base already appended "Logger+"; now we add the derived token.
ctorTrace += "Timestamp+";
}
// Destructor: derived runs BEFORE base (notes 24.3: reverse of construction).
// m_dtorTrace is a PROTECTED member in Logger, so we can access it here.
TimestampLogger::~TimestampLogger()
{
m_dtorTrace += "Timestamp-"; // protected base member — accessible to derived
}
// TASK 4: TimestampLogger::log().
//
// This REDEFINES Logger::log() — the name "log" in TimestampLogger HIDES the
// base version. (notes 24.7 / 24.8: function hiding)
//
// IMPORTANT: call Logger::log(decorated) — NOT log(decorated).
// - log(decorated) would call THIS function recursively (infinite loop).
// - Logger::log(decorated) calls the BASE class version explicitly.
// (notes 24.7: "calling the base version explicitly — use Base::fn()")
//
// The base Logger::log() handles the "[name] ..." formatting AND increments
// m_lineCount, so CountingLogger still works correctly even on a
// TimestampLogger (the counter lives in the shared Logger base portion).
//
std::string TimestampLogger::log(const std::string& msg)
{
// Step 1: decorate the message with our tag.
std::string decorated { m_tag + " " + msg };
// Step 2: hand off to the BASE implementation for formatting + counting.
// The explicit Logger:: qualifier is REQUIRED (notes 24.7).
return Logger::log(decorated);
}
// ─── CountingLogger (derived class) ──────────────────────────────────────────
// TASK 5: CountingLogger constructor + linesLogged().
//
// CountingLogger adds no new data — so the initializer list only calls the base
// constructor. The body appends "Counting+", giving the full ctorTrace
// "Logger+Counting+" — same base-first proof as TimestampLogger above.
//
CountingLogger::CountingLogger(const std::string& name,
std::string& ctorTrace,
std::string& dtorTrace)
: Logger { name, ctorTrace, dtorTrace }
{
ctorTrace += "Counting+";
}
CountingLogger::~CountingLogger()
{
m_dtorTrace += "Counting-"; // protected base member — accessible to derived
}
// TASK 5 (continued): linesLogged() — reads the protected base member.
//
// Returns m_lineCount — the PROTECTED base-class member. (notes 24.5)
//
// This compiles because CountingLogger is a DERIVED CLASS of Logger, and
// m_lineCount is declared protected. Public code CANNOT write:
// CountingLogger cl { ... };
// cl.m_lineCount; // ERROR: m_lineCount is protected
// But derived-class code (this body) can read it freely.
//
int CountingLogger::linesLogged() const
{
return m_lineCount; // PROTECTED base member — accessible from derived class
}
// ─── processLog (free function — the cliffhanger) ────────────────────────────
// TASK 6: processLog.
//
// Accepts a Logger BY REFERENCE and calls l.log(msg).
//
// THE CLIFFHANGER (notes 24.7 CS6340 tie-in):
//
// Even if you pass a TimestampLogger object here:
// TimestampLogger tl { ... };
// processLog(tl, "hello"); // <-- what runs?
//
// ... the call l.log(msg) resolves to Logger::log() — NOT
// TimestampLogger::log(). WHY?
//
// Because `l` is declared as `Logger&`. The compiler resolves the call
// at COMPILE TIME using the STATIC (declared) type of the reference — Logger.
// This is STATIC BINDING. The runtime object happens to be a TimestampLogger,
// but WITHOUT `virtual`, the compiler has no mechanism to dispatch to the
// derived version.
//
// The test that passes a TimestampLogger through this function DELIBERATELY
// asserts the BASE behavior — "[name] msg" WITHOUT the timestamp tag. This is
// the "expected but disappointing" result that makes Chapter 25 necessary.
//
// THE FIX (Chapter 25): add `virtual` to Logger::log(). Then the vtable
// dispatches at RUNTIME using the DYNAMIC (actual) type of the object, and
// TimestampLogger::log() runs. But that is Chapter 25's story.
//
std::string processLog(Logger& l, const std::string& msg)
{
return l.log(msg); // STATIC BINDING: always calls Logger::log(),
// even if `l` refers to a TimestampLogger.
}
Try the lab first — the learning is in the attempt.
// ============================================================================
// solution/main.cpp — DEMO DRIVER for the report-logger family (Ch 24)
// ----------------------------------------------------------------------------
// This is an UNGRADED demo; the grade comes from `make test-solution`.
// Run it with: make solution
//
// The demo is deliberately designed to make EVERY key Ch 24 concept
// OBSERVABLE in the output — you can SEE the construction order, the
// different log formats, and the static-binding cliffhanger.
// ============================================================================
#include <iostream>
#include <string>
#include "logger.h"
int main()
{
std::cout << "============================================================\n";
std::cout << " Chapter 24 — Inheritance: Report-Logger Family Demo\n";
std::cout << "============================================================\n\n";
// ── 1. Basic Logger ───────────────────────────────────────────────────────
std::cout << "── 1. Basic Logger ─────────────────────────────────────────\n";
{
std::string ct, dt;
Logger base { "system", ct, dt };
std::cout << " Construction trace: \"" << ct << "\"\n";
std::cout << " name(): " << base.name() << "\n";
std::cout << " log(\"boot\"): " << base.log("boot") << "\n";
std::cout << " log(\"running\"): " << base.log("running") << "\n";
// At end of block: destructor runs, dtorTrace updated.
}
// (dtorTrace goes out of scope with the Logger — demo only observes ctor side)
std::cout << "\n";
// ── 2. Construction order proof (notes 24.3) ──────────────────────────────
std::cout << "── 2. Construction / destruction order (notes 24.3) ────────\n";
{
std::string ct, dt;
{
std::cout << " Constructing TimestampLogger...\n";
TimestampLogger tl { "audit", "[T]", ct, dt };
std::cout << " ctor trace: \"" << ct << "\"\n";
std::cout << " (base \"Logger+\" appears BEFORE derived \"Timestamp+\")\n";
}
std::cout << " dtor trace: \"" << dt << "\"\n";
std::cout << " (derived \"Timestamp-\" appears BEFORE base \"Logger-\")\n";
}
std::cout << "\n";
// ── 3. TimestampLogger::log() (notes 24.7) ────────────────────────────────
std::cout << "── 3. TimestampLogger::log() redefinition (notes 24.7) ─────\n";
{
std::string ct, dt;
TimestampLogger tl { "router", "[T]", ct, dt };
std::cout << " Direct call on TimestampLogger object:\n";
std::cout << " tl.log(\"connect\") = \"" << tl.log("connect") << "\"\n";
std::cout << " Tag \"[T]\" appears because the DERIVED log() runs.\n";
}
std::cout << "\n";
// ── 4. CountingLogger (notes 24.5, 24.6) ─────────────────────────────────
std::cout << "── 4. CountingLogger (notes 24.5 / 24.6) ───────────────────\n";
{
std::string ct, dt;
CountingLogger cl { "counter", ct, dt };
cl.log("startup");
cl.log("shutdown");
std::cout << " After 2 log calls, linesLogged() = " << cl.linesLogged() << "\n";
std::cout << " (linesLogged() reads protected m_lineCount directly)\n";
}
std::cout << "\n";
// ── 5. THE CLIFFHANGER — static binding (notes 24.7 CS6340 tie-in) ────────
std::cout << "── 5. THE CLIFFHANGER: static binding (notes 24.7) ─────────\n";
{
std::string ct, dt;
TimestampLogger tl { "router", "[T]", ct, dt };
std::cout << " Direct call on TimestampLogger OBJECT (derived log):\n";
std::cout << " tl.log(\"direct\") = \"" << tl.log("direct") << "\"\n";
std::cout << " -> tag \"[T]\" present: derived TimestampLogger::log() ran\n\n";
std::cout << " Call via processLog(Logger& l, msg) (static binding!):\n";
std::string result = processLog(tl, "via_ref");
std::cout << " processLog(tl, \"via_ref\") = \"" << result << "\"\n";
std::cout << " -> NO tag \"[T]\": Logger::log() ran (static type = Logger&)\n\n";
std::cout << " The OBJECT is still a TimestampLogger, but the call\n";
std::cout << " resolved at COMPILE TIME using the declared type Logger.\n";
std::cout << " This is STATIC BINDING — the problem Chapter 25 solves\n";
std::cout << " with `virtual`. Add `virtual` to Logger::log() and the\n";
std::cout << " tag reappears. That is Chapter 25.\n";
}
std::cout << "\n============================================================\n";
std::cout << " End of demo. Run `make test-solution` to grade the solution.\n";
std::cout << "============================================================\n";
return 0;
}
// ============================================================================
// tests/tests.cpp — automated grader for the report-logger family (Ch 24)
// ----------------------------------------------------------------------------
// Tiny no-framework harness (same style as the other drills). Includes
// ../logger.h and calls the API through BOTH the starter (make test) and the
// solution (make test-solution). Exit code 0 = PASS, 1 = FAIL.
//
// The Makefile links this file against either starter/logger.cpp or
// solution/logger.cpp via -I. (so #include "logger.h" resolves correctly).
//
// GRADING PHILOSOPHY FOR CH 24:
// - Tests are organized by concept to give clear diagnostic messages.
// - The STATIC BINDING test (Task 6) is the crown jewel: it DELIBERATELY
// asserts the BASE (non-timestamp) behavior when passing a TimestampLogger
// through a Logger&. The test is correct — the "wrong" (base-only) result
// IS the expected result for a non-virtual hierarchy. The comment explains
// why. (notes 24.7 CS6340 tie-in)
// ============================================================================
#include <iostream>
#include <string>
#include "logger.h" // found via -I. (Makefile); same header, both builds
static int fails = 0;
// CHECK: assert a boolean condition; on failure report expression and line.
#define CHECK(cond) \
do { if (!(cond)) { std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while (0)
// CHECK_EQ: compare two std::string values with a readable diff on failure.
#define CHECK_EQ(got, want) \
do { std::string g_ = (got), w_ = (want); \
if (g_ != w_) { std::cerr << "FAIL @line " << __LINE__ \
<< ": got \"" << g_ << "\", want \"" << w_ << "\"\n"; ++fails; } } while (0)
int main()
{
// ── SECTION 1: Basic Logger construction and name() ──────────────────────
//
// The simplest thing: Logger constructs, name() returns what was passed in.
// Exercises TASK 1 (ctor) and the public accessor.
{
std::string ctorTrace, dtorTrace;
Logger base { "base_log", ctorTrace, dtorTrace };
CHECK(base.name() == "base_log");
}
// ── SECTION 2: Logger::log() format and line count ───────────────────────
//
// Verifies TASK 2: the format "[<name>] <msg>", the return value, and that
// multiple calls accumulate the count correctly.
{
std::string ct, dt;
Logger lg { "sys", ct, dt };
// First message
CHECK_EQ(lg.log("boot"), "[sys] boot");
// Second message (different content — checks no state leakage)
CHECK_EQ(lg.log("running"), "[sys] running");
// Edge case: empty message
CHECK_EQ(lg.log(""), "[sys] ");
}
// ── SECTION 3: Construction order — BASE constructs FIRST (notes 24.3) ──
//
// This is THE observable proof of the chapter's core rule. The ctorTrace
// string is appended to by Logger's ctor first, then by the derived ctor.
// EXPECTED: trace starts with "Logger+" (base) before "Timestamp+" (derived).
//
{
std::string ct, dt;
{
TimestampLogger tl { "ts_log", "[T]", ct, dt };
// At this point the derived object is alive. Check the order.
}
// After the block: destructors have run in REVERSE order.
// Ctor trace must start with "Logger+" (base ran first).
CHECK(ct.substr(0, 7) == "Logger+"); // base constructed first
CHECK(ct == "Logger+Timestamp+"); // both tokens present
}
{
std::string ct, dt;
{
CountingLogger cl { "cnt_log", ct, dt };
}
CHECK(ct.substr(0, 7) == "Logger+"); // base constructed first
CHECK(ct == "Logger+Counting+");
}
// ── SECTION 4: Destruction order — REVERSE of construction (notes 24.3) ──
//
// After a TimestampLogger goes out of scope, the dtor trace should show:
// "Timestamp-" (derived dtor runs first) then "Logger-" (base dtor).
{
std::string ct, dt;
{
TimestampLogger tl { "ts_log", "[T]", ct, dt };
} // tl destroyed here
CHECK(dt == "Timestamp-Logger-"); // derived dtor first, then base
}
{
std::string ct, dt;
{
CountingLogger cl { "cnt_log", ct, dt };
}
CHECK(dt == "Counting-Logger-");
}
// ── SECTION 5: TimestampLogger::log() redefinition (notes 24.7) ─────────
//
// Calling log() on a TimestampLogger OBJECT goes to the derived version,
// which prepends the tag, then calls Logger::log() for the base format.
// The resulting string should include BOTH the tag and the base formatting.
//
{
std::string ct, dt;
TimestampLogger tl { "audit", "[T]", ct, dt };
// The base formatting wraps the tag+msg combination:
// msg "connect" -> decorated "[T] connect" -> base formats as "[audit] [T] connect"
CHECK_EQ(tl.log("connect"), "[audit] [T] connect");
// Different tag to verify the tag is actually used
TimestampLogger tl2 { "sec", "[ERR]", ct, dt };
CHECK_EQ(tl2.log("denied"), "[sec] [ERR] denied");
// Edge case: empty message
CHECK_EQ(tl.log(""), "[audit] [T] ");
}
// Verify that TimestampLogger::log CALLS the base (m_lineCount increments).
// The counter should go up on each log call, regardless of which log() ran.
{
std::string ct, dt;
CountingLogger cl { "counter", ct, dt };
CHECK(cl.linesLogged() == 0); // no calls yet
cl.log("a");
CHECK(cl.linesLogged() == 1);
cl.log("b");
cl.log("c");
CHECK(cl.linesLogged() == 3);
}
// ── SECTION 6: protected m_lineCount via CountingLogger (notes 24.5) ─────
//
// CountingLogger does NOT redefine log() — it inherits Logger::log().
// But it ADDS linesLogged() which reads the protected m_lineCount.
// This is the demonstration of protected: derived can read it, public cannot.
//
{
std::string ct, dt;
CountingLogger cl { "audit", ct, dt };
CHECK(cl.linesLogged() == 0); // before any log calls
cl.log("startup");
CHECK(cl.linesLogged() == 1);
cl.log("shutdown");
CHECK(cl.linesLogged() == 2);
// Edge case: verify CountingLogger still formats the same as Logger
// (it inherits log() unchanged — notes 24.6)
std::string ct2, dt2;
CountingLogger cl2 { "inf", ct2, dt2 };
CHECK_EQ(cl2.log("tick"), "[inf] tick");
CHECK(cl2.linesLogged() == 1);
}
// TimestampLogger routes through the SAME base Logger::log() (via the
// explicit base call), so its derived log() must keep returning correctly
// formatted, tag-prefixed entries across repeated calls — proof that the
// base formatting + counting path is reused, not duplicated. Neither
// TimestampLogger nor Logger exposes linesLogged() publicly (only
// CountingLogger adds it), so we assert on the visible return value here and
// rely on the CountingLogger section above for the count itself.
{
std::string ct, dt;
TimestampLogger tl { "ts", "[X]", ct, dt };
CHECK_EQ(tl.log("first"), "[ts] [X] first");
CHECK_EQ(tl.log("second"), "[ts] [X] second");
}
// ── SECTION 7: THE CLIFFHANGER — static binding (notes 24.7) ────────────
//
// This is the MOST IMPORTANT test in the exercise. Read the comment below
// carefully before concluding it is wrong — it is INTENTIONALLY testing the
// "disappointing" behavior.
//
// processLog(l, msg) accepts a Logger& and calls l.log(msg).
// When we pass a TimestampLogger:
//
// TimestampLogger tl { ... };
// processLog(tl, "hello");
//
// ... the call inside processLog resolves to Logger::log() — NOT to
// TimestampLogger::log() — because `l` is typed as `Logger&` and there is
// no `virtual`. This is STATIC BINDING: name lookup uses the DECLARED
// (static) type of the reference, not the RUNTIME type of the object.
//
// The CHECK below asserts the BASE format "[name] msg" WITHOUT the tag.
// This is CORRECT behavior for a non-virtual hierarchy. It is also the
// problem that makes Chapter 25 necessary.
//
// If you made log() virtual (forbidden in this chapter — it would make
// this test fail as designed), you would get "[name] [T] msg" instead.
// The failing test would be Chapter 25's reward.
//
{
std::string ct, dt;
TimestampLogger tl { "router", "[T]", ct, dt };
// Direct call on the TimestampLogger object: DERIVED version runs.
// (Correct and expected: calling on the actual object type.)
CHECK_EQ(tl.log("direct"), "[router] [T] direct");
// Call through Logger&: BASE version runs (static binding!).
// The tag "[T]" does NOT appear — even though the object IS a
// TimestampLogger. This is the cliffhanger.
std::string result = processLog(tl, "via_ref");
CHECK_EQ(result, "[router] via_ref"); // base format, NO tag
// Verify the counter incremented for BOTH calls (both routes through
// log() ultimately call Logger::log() which increments m_lineCount).
// We can't call linesLogged() on TimestampLogger, so we do an indirect
// check: make a CountingLogger and route through processLog.
std::string ct2, dt2;
CountingLogger cl { "cnt", ct2, dt2 };
processLog(cl, "msg1");
processLog(cl, "msg2");
CHECK(cl.linesLogged() == 2);
// Static binding with CountingLogger through Logger& also runs Logger::log.
// (CountingLogger doesn't redefine log(), so the result is the same as
// calling Logger::log() directly — in this case the behavior is correct.)
std::string ct3, dt3;
CountingLogger cl2 { "probe", ct3, dt3 };
CHECK_EQ(processLog(cl2, "probe_msg"), "[probe] probe_msg");
}
// ── SECTION 8: Edge cases ────────────────────────────────────────────────
{
// Logger with an empty name: "[" + "" + "] " + "x" = "[] x"
// (no space inside the brackets when the name is empty — that is correct)
std::string ct, dt;
Logger empty { "", ct, dt };
CHECK_EQ(empty.log("x"), "[] x");
}
{
// Multiple TimestampLogger objects with the same base tag can coexist;
// their counts are independent (different Logger base subobjects).
std::string ct, dt;
TimestampLogger a { "a", "[A]", ct, dt };
TimestampLogger b { "b", "[B]", ct, dt };
CHECK_EQ(a.log("hello"), "[a] [A] hello");
CHECK_EQ(b.log("world"), "[b] [B] world");
// a and b have independent base subobjects — no shared state.
}
// ── RESULT ───────────────────────────────────────────────────────────────
if (!fails)
std::cout << "PASS \xE2\x9C\x85 all logger checks passed.\n";
else
std::cerr << "\nFAIL \xE2\x9D\x8C " << fails
<< " check(s) failed — see lines above.\n";
return fails ? 1 : 0;
}
// ============================================================================
// logger.h — PUBLIC INTERFACE of the report-logger family (Chapter 24)
// ----------------------------------------------------------------------------
// This header is COMPLETE and PROVIDED. Do not edit it. Read it closely —
// it is the contract every file in this exercise (starter, solution, tests)
// includes. Both the learner's starter/logger.cpp AND solution/logger.cpp
// include this file; tests/tests.cpp includes it too and calls the API.
//
// THREE CLASSES ARE DECLARED HERE — study their is-a hierarchy:
//
// Logger (base class — owns a name, counts lines logged)
// |
// +-- TimestampLogger (derived — REDEFINES log() with a prefix tag,
// | then calls the base version via Logger::log())
// |
// +-- CountingLogger (derived — adds linesLogged() that reads the
// protected m_lineCount from the base class)
//
// KEY VOCABULARY (notes 24.1 – 24.2):
//
// - INHERITANCE (": public Base"): a derived class inherits the accessible
// members of the base class and can add or redefine behavior.
// - BASE/DERIVED (or parent/child, superclass/subclass — all mean the same).
// - PUBLIC INHERITANCE: the normal "is-a" form; a derived object CAN be used
// wherever a base-class REFERENCE OR POINTER is expected. (notes 24.2)
//
// HEADER GUARD (Chapter 2): the #ifndef / #define / #endif sandwich prevents
// this file's contents from being pasted in twice in the same translation unit.
//
// CS6340 / LLVM TIE-IN: LLVM's instruction hierarchy has exactly this shape —
// llvm::Instruction is the base; llvm::BranchInst, llvm::CallInst, etc. are
// derived classes that inherit behavior and add type-specific operations. You
// will read code like this every day of CS6340. (notes 24.1)
// ============================================================================
#ifndef LOGGER_H
#define LOGGER_H
#include <string> // std::string (Chapter 5)
// ─── Base class: Logger ──────────────────────────────────────────────────────
//
// Logger is the general concept: any log sink that has a name, produces a
// formatted log string for a message, and tracks how many lines it has logged.
//
// DESIGN NOTES (notes 24.5):
// - m_name is PRIVATE: only Logger's own code touches it. Derived
// classes get the name through name() (the public accessor).
// - m_lineCount is PROTECTED: derived classes (e.g. CountingLogger) are
// allowed to READ it directly — that is precisely what protected
// is for. Public users cannot touch it.
// - log() is PUBLIC: the service this class provides. A derived class
// may REDEFINE it (a same-name function in the derived class hides
// the base version for calls on a derived object — notes 24.7).
//
// CONSTRUCTION-ORDER OBSERVABLE (notes 24.3 – 24.4):
// The constructors append tokens to a shared trace string so the tests can
// PROVE that the base constructs first, then the derived portion.
// The pattern: pass trace by reference to both ctor and dtor (stored as a
// protected member so derived classes can also append without storing a
// second copy of the same reference).
//
class Logger
{
public:
// ── Constructor / destructor ─────────────────────────────────────────────
//
// explicit: this is a multi-argument constructor, so it can never be picked
// for a single-value implicit conversion (there is no lone value that turns
// into a Logger). What `explicit` blocks here is COPY-LIST-INITIALIZATION,
// e.g. Logger l = { name, ct, dt }; — that form is rejected, while the
// direct form Logger l { name, ct, dt }; is still fine. It is good habit
// to mark constructors explicit by default. (Chapter 14 — provided
// scaffolding, not a learner TASK.)
//
// ctorTrace / dtorTrace are output parameters: each constructor and
// destructor body APPENDS a short token so the caller can observe the
// construction order WITHOUT std::cout. (notes 24.3: "the base portion must
// be initialized before the derived portion can safely use it.")
//
explicit Logger(const std::string& name,
std::string& ctorTrace,
std::string& dtorTrace);
~Logger();
// ── Public interface ─────────────────────────────────────────────────────
// name() — read the logger's name (Chapter 15: const member function).
const std::string& name() const;
// log(msg) — format msg as "[<name>] <msg>", APPEND that string to the
// internal log, INCREMENT m_lineCount, and RETURN the formatted string.
// Derived classes may REDEFINE this function to change the format while
// still calling this version for the base formatting work. (notes 24.7)
std::string log(const std::string& msg);
protected:
// ── Protected members — visible to derived classes, hidden from public ────
//
// m_lineCount is protected so CountingLogger can READ it directly
// (demonstrating notes 24.5: "protected exists mainly for inheritance").
int m_lineCount {};
// m_dtorTrace is protected so derived destructors can append their token
// without storing a second copy of the reference. Destructors run in
// REVERSE order (notes 24.3): derived destructor appends FIRST, then the
// base destructor appends.
std::string& m_dtorTrace;
private:
// ── Private data — Logger's own state, not accessible to derived classes ─
std::string m_name;
std::string m_log; // accumulated log text
};
// ─── Derived class: TimestampLogger ──────────────────────────────────────────
//
// TimestampLogger IS-A Logger with a twist: its log() PREPENDS a fixed tag
// (the "timestamp" — e.g. "[T]") before delegating to the base Logger::log().
//
// KEY CONCEPTS demonstrated here (notes 24.7):
//
// 1. FUNCTION REDEFINITION (hiding): TimestampLogger::log() has the same name
// as Logger::log(). Calls on a TimestampLogger OBJECT go to the derived
// version — but calls through a Logger& still go to Logger::log().
// (This is STATIC BINDING — notes 24.7 CS6340 tie-in. Task 6 makes it
// concrete and disappointing on purpose.)
//
// 2. BASE CALL SYNTAX: inside TimestampLogger::log(), you must call the base
// version as Logger::log(msg) — NOT just log(msg) (which would
// recurse into itself). (notes 24.7)
//
// 3. DERIVED CONSTRUCTOR: initializes its OWN data AND calls the base ctor
// in its MEMBER INITIALIZER LIST. (notes 24.4)
//
class TimestampLogger : public Logger // ": public Logger" = public inheritance
{
public:
// The derived constructor MUST name the base constructor in the initializer
// list. The base portion constructs first (notes 24.3 / 24.4).
TimestampLogger(const std::string& name,
const std::string& tag,
std::string& ctorTrace,
std::string& dtorTrace);
~TimestampLogger();
// REDEFINES Logger::log(). Call the base version with Logger::log().
// (notes 24.7: "calling the base version explicitly")
std::string log(const std::string& msg);
private:
std::string m_tag; // the prefix tag, e.g. "[T]"
};
// ─── Derived class: CountingLogger ───────────────────────────────────────────
//
// CountingLogger IS-A Logger that adds one public getter: linesLogged().
// It does NOT redefine log() — it inherits the base behavior unchanged.
//
// KEY CONCEPT: adding new functionality (notes 24.6) and accessing a
// PROTECTED BASE MEMBER (notes 24.5: m_lineCount is directly readable
// because it is protected).
//
class CountingLogger : public Logger
{
public:
CountingLogger(const std::string& name,
std::string& ctorTrace,
std::string& dtorTrace);
~CountingLogger();
// linesLogged() returns the number of messages logged so far.
// It accesses the base class's protected m_lineCount directly — that is
// the whole point of protected (notes 24.5).
int linesLogged() const;
};
// ─── Free function: processLog ────────────────────────────────────────────────
//
// Accepts a Logger BY REFERENCE and calls l.log(msg).
//
// TASK 6 (THE CLIFFHANGER): the parameter type is "Logger&". When you pass a
// TimestampLogger through a Logger& here, STATIC BINDING ensures that
// Logger::log() runs — NOT TimestampLogger::log(). The object IS a
// TimestampLogger, but the call is resolved at compile time using the
// DECLARED (static) type of the reference. This is the central limitation that
// Chapter 25 — virtual functions — exists to solve. (notes 24.7 CS6340 tie-in)
//
std::string processLog(Logger& l, const std::string& msg);
#endif // LOGGER_H
# Chapter 24 — Inheritance · Report-Logger Family · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# The learner edits starter/logger.cpp (three class bodies + processLog).
# The header logger.h is the CONTRACT: complete and provided.
# tests/tests.cpp includes ../logger.h and calls the API through BOTH
# the starter and the solution. The grader is linked against the
# appropriate .cpp with $(CXX) in one command.
#
# -I. puts the chapter root on the include path so every file can write
# #include "logger.h" (no path prefix needed)
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── Starter: compile-check the learner's logger.cpp (warning-clean object) ──
build:
$(CXX) $(CXXFLAGS) -c starter/logger.cpp -o starter/logger.o
@echo "OK ✅ starter/logger.cpp compiles. Now run: make test"
# ── run — build and run the starter's demo main ─────────────────────────────
run: build
$(CXX) $(CXXFLAGS) starter/main.cpp starter/logger.cpp -o starter/demo
./starter/demo
# ── test — grade the LEARNER's code ─────────────────────────────────────────
# RED until the TASK blocks are filled in; GREEN once they are correct.
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/logger.cpp -o tests/run
@./tests/run || echo "FAIL ❌ fill in the TASK blocks in starter/logger.cpp until every check passes."
# ── solution — build + run the reference demo ────────────────────────────────
solution: solution/demo
./solution/demo
solution/demo: solution/main.cpp solution/logger.cpp
$(CXX) $(CXXFLAGS) solution/main.cpp solution/logger.cpp -o solution/demo
# ── test-solution — proof the lab is solvable ────────────────────────────────
# The reference solution MUST pass every check.
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/logger.cpp -o tests/run
@./tests/run
clean:
rm -f starter/logger.o starter/demo solution/demo tests/run
rm -rf starter/demo.dSYM solution/demo.dSYM tests/run.dSYM
make test locally
(see “Build & run locally” above).