Chapter 23 of 24 | C++
A project ladder
Projects connect isolated language features into useful software. Start small, finish a working version, and add complexity only when the current version is reliable. Each project below is designed to reuse earlier chapters and create a concrete artifact you can test.
Stage 1: command-line utilities
- Number guessing game: use input validation, comparisons, loops, and a random-number facility. Define what happens when input is malformed.
- Unit converter: model units explicitly, split conversion logic into functions, and test known values and boundaries.
- Text statistics: read a file line by line and count lines, words, or bytes. State how you define a word and handle encoding.
Stage 2: structured data and persistence
- Contact book: create a record type, store records in a vector, search and sort them, and validate user actions.
- Expense tracker: represent money in integer minor units, calculate totals, and save/load a documented file format.
- Task manager: model task states with an enumeration, keep responsibilities in small functions, and report file errors clearly.
Stage 3: reusable components
- Library catalog: separate data modeling, searching, persistence, and the command-line interface into modules or source/header pairs.
- Log analyzer: parse records defensively, use standard algorithms and maps, and compare performance on representative input.
- Small game or simulation: define state transitions and invariants, then test them independently of rendering or input.
A project checklist
- Write down the user-visible requirements and the invalid inputs.
- Choose data types and ownership before implementing every detail.
- Keep the first version small and runnable; add one feature at a time.
- Test core behavior and failure paths, then build from a clean directory.
- Include a README with purpose, requirements, build, run, and test steps.
For every project, explain the design trade-offs you made. A finished small tool with clear behavior is more valuable than a large unfinished program.
Plan a project in thin slices
Begin with a short description of the user and the task they need to complete. Write down what the program accepts, what it produces, and what should happen when the input is missing, malformed, or outside the allowed range. This scope prevents an early project from expanding into an impossible collection of features.
Implement a thin end-to-end path first. A text-statistics tool might initially read one named file, count its lines, and print the result. Once that path works, add word counting, better error messages, tests, and support for more input options. Each slice should leave the program buildable and usable.
Example design: contact book
Start by defining one contact's data: perhaps a display name, phone number, and email address. Decide which fields may be empty and whether two contacts can have the same name. Store records in a vector for the first version, because it is easy to traverse, append to, and sort. Use functions for adding, searching, displaying, and loading contacts so the menu code does not contain every rule.
Next specify persistence. A naive comma-separated format fails when a field itself contains a comma, quote, or newline. Either define and test escaping rules, use a robust library for a known format, or choose a simpler format with clearly constrained fields. Always check file-open and parse errors, and do not silently discard malformed records.
Example design: expense tracker
Represent an amount as an integer number of minor units, such as cents, rather than a binary floating-point value. Define the allowed amount range and whether negative values represent refunds. Keep parsing and formatting at the user-interface boundary, then perform calculations on the validated integer representation.
Write tests for adding an expense, computing a total, and handling an empty collection. Include a large value near the representable range so overflow assumptions are visible. Save data in a format that includes a version or can be migrated if the record shape changes later.
Example design: small game or simulation
Represent the game state as data and write functions that transform a state in response to an action. This allows rules to be tested without reading from a keyboard or drawing a screen. State invariants explicitly: for a grid game, a cell should have one of a fixed set of states, and a move may only change a valid empty cell.
Separate deterministic rules from nondeterministic input and randomness. A seeded random generator can make a bug reproducible. Once the core rules pass tests, add presentation, persistence, or a more sophisticated interface.
Document and finish
A project is easier to share when another person can learn what it does, install its requirements, build it, run it, and execute its tests from the documentation. Include an example of expected input and output. Mention known limitations rather than implying the program supports cases it does not handle.
Before calling a project complete, try it from a clean working directory, test its failure paths, and remove temporary diagnostics. Ask someone else to follow the instructions without your help; confusing steps are useful feedback for improving the project.
Practice
Choose one project and write a one-page specification. Implement the smallest complete version, add tests for its core rules, and ask another person to follow your setup instructions from a clean checkout.
- Write three user stories and list the invalid inputs for your chosen project.
- Choose a data model and explain who owns each piece of data.
- Implement one testable rule before building the full user interface.
- Describe one feature you deliberately postponed and why.
Key idea: learn by finishing, testing, and explaining complete programs—not by collecting syntax alone.
Advanced capstone: build a small library
Choose one stable problem domain—a parser, geometry toolkit, statistics module, or task scheduler—and define a narrow public API. Separate interface from implementation, write contract tests, and document ownership and error behavior. Add a consumer executable that uses only the public interface; this catches accidental dependence on private headers.
Version the API and decide how incompatible changes will be handled. If binary compatibility matters, investigate ABI exposure, symbol visibility, and opaque implementation techniques. Build and test the library on at least two supported toolchains or configurations, and state what portability is not guaranteed.
Advanced capstone: profile and improve a real workload
Create a baseline with representative data and a correctness oracle. Profile the complete program to identify a bottleneck, then form a hypothesis about the cause. Change one design choice—algorithm, container, memory layout, or batching—and compare correctness, throughput, memory use, and tail behavior. Preserve the benchmark source and environment so a future contributor can reproduce the claim.
Do not report only the best timing. Include repeated measurements and variation, explain what work the benchmark includes, and check that compiler optimization did not remove the operation. If the improvement is small or unstable, report that honestly and prefer the simpler design.
Advanced capstone: test a parser adversarially
Define the accepted grammar and resource limits for an input format. The parser should reject malformed encoding, impossible lengths, invalid numeric ranges, and unsupported versions without out-of-bounds access or unbounded allocation. Add unit tests, property tests, fuzzing, and sanitizer builds. A successful parse should produce a valid domain object, not partially initialized state.
Review how the parser behaves under denial-of-service inputs such as huge repeated fields or deeply nested structures. Correctness includes bounded resource behavior when the input is untrusted. Document limits and ensure errors are distinguishable from valid empty data.
Research-style project practice
For an ambitious project, write a question that can be investigated rather than a feature list alone. Examples include “Which representation reduces memory traffic for this workload?” or “How do two compilers implement this standard feature?” Define a baseline, variables, metrics, and limitations before collecting results. Separate observed results from explanations and distinguish empirical evidence from standards guarantees.
A strong technical report includes reproducible source, environment details, methodology, results, threats to validity, and a conclusion proportional to the evidence. This discipline is useful in research and in ordinary engineering decisions.
Advanced capstone checklist
- State the problem, users, constraints, and non-goals.
- Design types and interfaces around invariants and ownership.
- Automate builds, tests, static checks, and at least one sanitizer configuration.
- Measure a baseline before optimizing; preserve the benchmark and data.
- Document portability assumptions, dependencies, licensing, and known limitations.
- Write a short report that another engineer can reproduce and critique.