Skip to content

Practice Lab 5 — Employee record with a lifecycle trace

Difficulty: Beginner · Estimated time: 3 hours · Module 3: Constructors, Destructors and the Object Lifecycle

You will be asked to write the output down before you run the program. Being wrong is the point: the gap between what you expected and what happened is the lesson.


At a glance

Difficulty Beginner
Estimated time 3 hours
Module Module 3 — Constructors, Destructors and the Object Lifecycle
Concepts practised Default and parameterised constructors, constructor overloading, member initialisation lists, destructors, object lifetime, scope, temporary objects
Prerequisite Practice Lab 4
Class design given — one class, named for you

Scenario

An HR system keeps employee records. The system's authors are debugging a resource problem: an employee record is supposed to write a log line when it is created and another when it is destroyed, and the log has more lines than there are employees.

Nobody believes the count. That is why every constructor and destructor in this lab narrates itself.


Problem statement

Implement a class Employee with:

  • a name, an employee number, a department, and an annual salary in whole pence;
  • a default constructor that produces a record with an obvious placeholder identity;
  • a parameterised constructor taking name, number, department and salary;
  • a constructor that takes name and number only, delegating to the four-argument one with sensible defaults for the rest;
  • a destructor that narrates itself;
  • a describe() operation.

Every constructor and the destructor print one line, in a fixed format:

[ctor ] #1001 Ayesha Khan (Data Science) 4,500,000 pence
[dtor ] #1001 Ayesha Khan

Then write a main that produces a fixed, deliberate sequence of object lifetimes:

  1. A named local object.
  2. An unnamed temporary passed to a function that takes Employee by value.
  3. An object inside a nested block, so that it dies before main ends.
  4. A named object created with Employee{} — a temporary used to initialise it.
  5. A fixed array of three employees.

Run it, and in lifecycle-trace.md write down the order you expected before running it, then the actual order, then the differences.


Learning objectives

By the end of this lab you will be able to:

  • Write default, parameterised and delegating constructors, and say when each is used.
  • Use a member initialisation list and explain why it is not the same as assigning in the body.
  • Predict the order of construction and destruction for locals, nested scopes, temporaries and arrays.
  • Explain why an object passed by value produces two narrations, and what to pass instead when copying is not intended.
  • Say what a destructor is for, and name one thing this lab's destructor does not need to do.

Functional requirements

  1. The placeholder default record is clearly invalid: an empty name, number 0, and a department of unassigned — and it must not pretend to be a valid employee.
  2. The delegating constructor produces a record with the same defaults as the default constructor for the fields it does not take — prove it by comparing the two describe() outputs.
  3. The initialisation list is used for every member that can be initialised there.
  4. A constant salary default for a department, if you choose to add one, is a static constexpr member and is not per-object.
  5. The narration format is fixed, so two runs are comparable, and every line ends with a newline.
  6. Destruction narration reports the same identity as construction.
  7. Every describe() is const.

Constraints

  • Two files: employee.h and main.cpp (put the definitions inline for now; a .cpp split is Module 2's exercise, and a separate file here would hide the narration in a different file from main).
  • One class, no inheritance, no smart pointers.
  • -Wall -Wextra -Werror, no warnings.
  • The salary is integer pence. Formatting with thousands separators is optional; if you do it, do it in describe only.
  • No std::endl — use '\n', and say in your notes why that choice is about flushing, not style.

Starter task

Before coding, write in lifecycle-trace.md:

  1. The exact expected narration order for the five parts of main, as line numbers with ctor/dtor.
  2. How many ctor lines you expect in total, and how many dtor lines.
  3. The answer to this: passing an Employee by value to a printing function — one narration or two? Write your expectation down.

Then implement, run, and compare. Record every surprise; do not edit the expectation afterwards.


Implementation tasks

  1. Declare the class with its four fields, including a default value for each where the default constructor would otherwise do nothing.
  2. Write the three constructors, with the two-argument one delegating.
  3. Write the narration in one helper function so the format cannot drift between constructors.
  4. Write describe.
  5. Write main with the five parts, in the given order.
  6. Run it and record the output.
  7. Repeat part 2 with the parameter taken by const reference instead of by value, and record how the count changes.

Test cases

# Action Expected result
1 Create a default employee One ctor line with the placeholder identity; describe reports unassigned
2 Create a four-argument employee One ctor line with the real values
3 Create a two-argument employee One ctor line, and the same defaults as case 1 for the missing fields
4 Pass an employee by value to a printing function Two narrations: the copy and the original both narrate
5 Pass the same employee by const reference Zero extra narrations
6 A nested block with one employee Its dtor prints before the enclosing scope's
7 Employee e = Employee{} A temporary narrates, then the named object
8 A fixed array of three Three ctor lines, then three dtor lines in reverse order
9 End of main Everything constructed is destroyed exactly once
10 Count the lines ctor count equals dtor count

Boundary cases to run deliberately: an empty name and a name with spaces; the array of exactly one element; a scope that throws (you may defer this to Module 13, but note the expectation).


Expected behaviour

Shape of the output — the order is what matters, and you must produce it yourself:

[ctor ] #0 (unassigned) 0 pence
[ctor ] #1001 Ayesha Khan (Data Science) 4500000 pence
[ctor ] #1002 Bilal Ahmed (Operations) 0 pence
[ctor-copy] #1001 Ayesha Khan
[dtor ] #1001 Ayesha Khan
...
[dtor ] #1002 Bilal Ahmed
ctor lines: 9
dtor lines: 9

Extension challenge

Give Employee a department-default salary, so that the two-argument constructor produces a salary based on the department.

Then record:

  • Where the salary table lives (a static member, a free function, a class of its own) and why.
  • Which constructor had to change, and how many callers would notice if the table changed.
  • The narration lines added, and whether the count of ctor and dtor lines is still equal.

Debugging challenge

The destructor never runs for one of the objects:

Employee* make_employee() {
    Employee local("Zainab", 1003, "Finance", 3800000);
    return &local;      // "the caller will destroy it"
}

Reproduce it and answer:

  1. Where does the narration for local appear, and where does the program then read that object?
  2. What is the lifetime of local, in one sentence?
  3. Which of these is the right fix: return by value, return a std::unique_ptr, or take an output parameter by reference? Name the trade-off each one makes, and implement the one you choose.
  4. Write down what would happen if you did destroy the returned object.

Reflection questions

  1. Which of your constructors did work that should have been in a member initialisation list, and what did you notice when you moved it?
  2. Why does the array's destruction order differ from its construction order, and where is that guaranteed?
  3. Passing by value produced two narrations. What else about a class makes that expensive, beyond the line it prints?
  4. Your destructor narrates. Name one thing a destructor in a real system does that a narration would never reveal.
  5. A static constexpr salary default is shared by every object. What would go wrong if it were a plain member instead?
  6. Employee{} in case 7 is a temporary. What is the difference between Employee e = Employee{}; and Employee e{};, and does it show in the narration?

Completion checklist

  • [ ] It compiles with -Wall -Wextra -Werror and no warnings
  • [ ] Three constructors, one of them delegating, each narrating in the same format
  • [ ] The initialisation list is used for every member it can be
  • [ ] The destructor narrates the same identity as the constructor
  • [ ] Every describe is const
  • [ ] A prediction of the narration order was written down before running
  • [ ] The actual output was compared with the prediction, and every difference is explained
  • [ ] ctor lines and dtor lines are equal in number, and the count is recorded
  • [ ] All ten test cases run, including both the by-value and by-reference variants
  • [ ] The debugging challenge was explained before being fixed, and the chosen fix's trade-off is written

Related: Lab index · Module 3 — Constructors and Destructors · Previous: Practice Lab 4 · Next: Practice Lab 6 — Sensor register: constructors and temporaries