Chapter 24 · Inheritance
Exercise · Chapter 24

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

  1. 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+" to ctorTrace. This is the observable proof that the base constructs first (notes 24.3).

  2. Logger::log(msg) (TASK 2). Return the formatted string "[" + m_name + "] " + msg. Increment m_lineCount and append entry + "\n" to m_log. Return the entry. This function is called by TimestampLogger::log() via the explicit Logger::log(msg) syntax and also inherited unchanged by CountingLogger.

  3. TimestampLogger constructor (TASK 3). In the member-initializer list, call Logger { name, ctorTrace, dtorTrace } (the ONLY way to initialize the base subobject — notes 24.4) and initialize m_tag { tag }. Append "Timestamp+" in the body. Full trace becomes "Logger+Timestamp+".

  4. TimestampLogger::log(msg) (TASK 4). Prepend the tag: build decorated = m_tag + " " + msg. Then call Logger::log(decorated) — the explicit base-call syntax is required; plain log(decorated) would recurse forever (notes 24.7). Return the result from the base.

  5. CountingLogger::linesLogged() (TASK 5). Return m_lineCount — the protected base-class member. This compiles because CountingLogger is a derived class; public users cannot access m_lineCount directly (notes 24.5). (The CountingLogger constructor body is provided, already correct.)

  6. processLog(l, msg) — the cliffhanger (TASK 6). A one-liner: return l.log(msg);. The test intentionally passes a TimestampLogger through Logger& l and asserts the base format — no tag — because static binding resolves the call using l'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 order
  • dt == "Timestamp-Logger-" — destruction reverses construction order (notes 24.3)
  • tl.log("connect") == "[audit] [T] connect" — derived log() prepends tag then calls base; tag AND base format both present (notes 24.7)
  • cl.linesLogged() == 2 after two calls — protected m_lineCount readable from derived (notes 24.5)
  • processLog(tl, "via_ref") == "[router] via_ref"NO tag — static binding runs Logger::log() even when the object is a TimestampLogger (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)
  • protected accessm_lineCount is protected so CountingLogger can read it directly; public users cannot (notes 24.5)
  • Adding new functionalityCountingLogger adds linesLogged() without redefining log() (notes 24.6)
  • Function redefinition (hiding)TimestampLogger redefines log(), hiding the base version for calls on a TimestampLogger object (notes 24.7)
  • Calling the base version explicitlyLogger::log(msg) inside TimestampLogger::log() (notes 24.7)
  • Static binding's limits — a call through Logger& always resolves to Logger::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)
  • const member functions (Ch 14–15)
  • std::string and += concatenation (Ch 5)
  • Reference parameters and returning by value (Ch 12)
  • Header guards and the .h/.cpp split (Ch 2)
Constraints
  • Allowed: everything through Chapter 23; public inheritance (: public Base); protected members; calling Base::fn() explicitly; adding new members to derived classes; std::string and +=/+.
  • 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) (not log(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
shell
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 artifacts

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

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

C++
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()
C++
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
C++
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
C++
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:

C++
CHECK_EQ(result, "[router] via_ref");   // NO tag "[T]" — static binding

is 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_lineCount from a protected data member to a private counter with a protected accessor function (int lineCount() const), and update CountingLogger::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 to TimestampLogger to explore what happens to the base overload set when a derived class introduces its own log (notes 24.7, 24.8).
  • Add virtual to Logger::log() (Chapter 25 preview) and watch the cliffhanger test flip: the processLog test will now fail because TimestampLogger::log() runs and the tag reappears. That is the fix Chapter 25 exists to provide.
  • Build a three-level chain: derive TimestampCountingLogger from TimestampLogger, adding both the tag and linesLogged(). 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 C++
// ============================================================================
//  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)
}
// ─────────────────────────────────────────────────────────────────────────────
Run
Submit
Run in your browser — coming soon For now: copy or download the files and use make test locally (see “Build & run locally” above).