C++ Programming
Lesson 01 of 24

Lesson 01 · 24 lesson course

The Appropriate Computer Programmer

Programming culture

Programming glossary 84 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Abstract balance of interconnected engineering systems representing correctness, testing, performance, tools, and human judgement.

7 Rules

Graham Morgan (graham@staticcarnival.com)

Just because someone can write a computer program does not indicate they are a computer programmer. This is quite an obvious statement to make, as you wouldn’t describe everyone who can operate a pair of scissors as a hairdresser. Only the ability to leave a head of hair looking styled once cut would qualify someone as a hairdresser. Similarly, only the ability to write a computer program that is appropriate and satisfies requirements would qualify someone as a computer programmer. However, due to an apparent shortage of computer programmers, there is an abundance of novice amateurs being paid as if they are computer programmers. This is one of the reasons why the world has no shortage of poorly written, error-ridden computer program code.

Here, we attempt to remedy the proliferation of novice amateur computer programmers who are ruining quite reasonable and, on the whole, well-thought-out hardware with their dreadful software. We do this by describing a set of rules associated with being an appropriate computer programmer. Novice amateurs typically ignore these rules, either wholly or partially. If these rules are adhered to and the resultant program code is still inappropriate, then a change in profession may be warranted. However, this would be a rare occurrence if appropriate effort and determination were applied.

Programming Insight (AI) — The New Division of Labour
  • Treat AI as your junior dev team: you architect, it drafts.
  • Use AI to generate alternatives (designs, APIs, data structures), then you select and refine.
  • Keep a traceable workflow: prompt → draft → test → review → integrate.

As program code becomes increasingly easy to generate, this distinction becomes more important rather than less. Students are not learning C++ merely so that they can spend their careers typing C++ programs. They are learning C++ so that they understand computation deeply enough to specify, inspect, reason about, challenge and direct systems that they may not have written line by line. A generated program is still either appropriate or inappropriate; responsibility for making that judgement remains with the programmer.

Following these rules may not guarantee that you will become an expert computer programmer. However, not following these rules will ensure you will never become an expert computer programmer.

As a side note: My inability to find a succinct introduction on the culture of programming (one written by someone who can program anyway) prompted me to write this article.

Theory and Practice

Theoretical computing science is quite abstract and typically overlooked by the novice programmer. However, at the very heart of the subject lies the most fundamental guidance one should take note of if one is to become a programmer beyond that of a novice amateur. Such guidance alone would not be sufficient to bring professional excellence to one's programming skills. Achieving excellence in programming requires theoretical understanding coupled with knowledge based on practical experience.  In this article, the theory is briefly combined with a practical knowledge of programming to construct several rules that should govern the expert computer programmer.

Before we consider our “rules of programming”, some individuals find it helpful to have books available to aid in the learning process. Therefore, to ensure a programmer does not burden everyone around them with, error prone, inefficient and generally dreadful code I recommend two series of books (there are no other books worth reading apart for reference material): first, the quite excellent series written by Donald Knuth – “The Art of Computer Programming” that comes in 4 volumes (so far); secondly, any reasonably priced book on an introduction to assembler programming (it doesn’t matter what underlying architecture is targeted, but it may be worth making sure you have the architecture available so you can at least program some examples). I repeat my previous statement: no other books are worth reading unless you need a reference guide (e.g., “The C++ Programming Language” - Bjarne Stroustrup) and only then if you have trouble using search engines and reading web pages.

Programming Insight (AI) — Turning Theory into Working Code
  • Ask AI to derive candidate algorithms with notes on complexity.
  • Generate proof sketches or invariants for you to formalise.
  • Use AI to integrate theory + code + tests in one notebook.

Performance

Some people consider the existence of a machine that can discover the solutions to mathematical problems useful. Allowing such a machine to be built so that it may accommodate a wide variety of problems with no alteration to its initial construction is considered advantageous. Assuming this “general purpose” machine should only require alterations to its instruction and data, the fundamental basic question one asks is ………

“Is it theoretically possible to provide a general-purpose machine that may solve, and know it has solved, all mathematical problems by altering its instruction and data alone?”

However, on reflection, it is not this question one really seeks an answer to. What we really want to know is ………

“Is it theoretically possible to provide a general-purpose machine that may solve, and know it has solved, all mathematical problems in polynomial time by altering its instructions and data alone?”

Simply put, is the time taken to compute a solution known and proportional to the size of the problem? For example, if it takes 10 seconds to sort 10 numbers, how long will it take to sort 10,000 numbers?

Programming Insight (AI) — Estimating & Measuring at Scale

This is very important and provides the first fundamental rule all computer programmers must adhere to:

  • Rule 1 - Be capable of estimating the timeliness of your computation
Programming Insight (AI) — From Big-O to Wall-Clock
  • Generate input sets that expose algorithmic regimes.
  • Ask AI for CSV/plot templates to chart N vs time.
  • Write short AI-assisted explanations of practical complexity.

Time matters

We are considering the notion of computability when deriving our first rule. Computability poses the most fundamental, and lucrative, question in computer science: Does P = NP? In essence, P represents those problems that can be solved deterministically in polynomial time, whereas NP represents those problems that can be solved non-deterministically in polynomial time. For example, I could sort 10 numbers deterministically, or I could keep swapping them around at random until I get them in the correct order. Notice that a deterministic algorithm is helpful to ascertain the correctness of the guesses.

A computer programmer should know what the upper limit (in computational steps) a problem would take to solve. However, if the computer programmer does not know this value, then either (1) they don’t know what they are doing, or (2) the problem is generally known not yet to have an algorithmic solution that falls within P. Make sure the answer is always (2).

Algorithms dictate performance

The most valuable information that code analysis brings is a deep understanding of the performance of an overall solution, regardless of hardware interference. The number of times I have seen programmers, often highly paid and respected by their colleagues, trying to optimise a piece of inefficient code (e.g., quicksort instead of insertion sort) is just too many. The foolish notion that a good programmer knows more about the “ins and outs” of the most obscure hardware execution primitives rather than the algorithmic nature of an appropriate solution is all too prevalent in today’s industry.

Ensuring code may be analysed sufficiently well for performance purposes also encourages the consideration of correctness. When considering a computed solution as either correct or incorrect, one must first understand the nature of error, failure and correctness.

Programming Insight (AI) — Choosing the Right Primitive

Correctness

An error (sometimes called a bug) occurs when a piece of code is constructed in such a way as to eventually invalidate, corrupt or remove one or more expected outputs while assuming the program code is actually finished! (We expect errors during program code creation – it is how we learn and hone our solutions). A programmer creates such errors for the following reasons: (1) ignorance of the problem itself, (2) inability to derive a suitable algorithmic solution, and (3) laziness. If a programmer suffers from (1) or (2), then they need to either become more learned in the art of programming or give up and pursue another career. More than likely, errors are a result of (3).

Programming Insight (AI) — Designing for Correctness First

Ensuring your code is correct requires two further rules for consideration:

  • Rule 2. Understand what a program is actually doing in a computational sense
  • Rule 3. Use automated and human-based approaches to evaluate correctness
Programming Insight (AI) — Two-Layer Evaluation
  • Layer 1: Automated checks (unit/property/fuzz) scaffolded by AI.
  • Layer 2: Human reading + debugger, guided by AI hints.
  • AI suggests sanitizer/test matrices for your platform.

Rules 2 and 3 apply regardless of who or what produced the implementation. If program code has been generated by AI, the programmer must still be able to explain the state it represents, the operations that change that state, the assumptions on which it relies, the points at which it may fail, and the evidence that supports accepting it. Plausibility is not correctness. The ability to generate program code quickly increases, rather than removes, the need for human evaluation.

Living in turmoil

If a programmer has written a program and does not realise what some elements of their program code are doing, then all hope is lost, and such a programmer will perpetually live in turmoil. The main reason for this is that such a lack of knowledge is debilitating to such an extent that one feels little control over successfully mastering a solution. Lack of power is stressful, especially if one is expected to attain control and succeed. Therefore, in the grand scheme of acquiring solutions, a programmer needs to understand all that a program is computing.

Programming Insight (AI) — Taming Unknown Code
  • Feed third-party snippets to AI for summaries and risks.
  • Ask for safe RAII wrappers around unsafe APIs.
  • Generate quick docs and call-graphs for orientation.

Third-party code

If a programmer relies on another’s program code, then this should also be understood without excuse. Typical excuses when using buggy “third-party” program code provided by programmers are: I assumed it was correct (and didn’t bother checking it); I didn’t write it, so I treat it like a “black box”; the person who wrote it is to blame. All such excuses display ignorance and/or laziness. If the third-party code is accessible and updateable, then it should be fixed, evaluated, and reintegrated into a solution. If the third party code is inaccessible and not updateable (sometimes third party code is provided in such a way) then the programmer of the third party code should be informed and such code should be disposed of. If the programmer of such code is either anonymous or impossible to track down, then this usually indicates that they want nothing to do with their own results and are probably living in shame. The least the professional programmer can do when greeted with such incompetently written code is to inform other programmers (possibly worldwide) to prevent other poor unfortunates from being burdened.

AI-generated program code should be treated with the same professional caution as any other code not written personally. Its origin does not excuse a lack of understanding. The programmer must establish the contract, inspect the relevant behaviour, test the assumptions, and reject an implementation that cannot be made understandable and challengeable. State models, invariants, execution traces, interfaces and selected C++ fragments provide human-readable manifestations of the computation, even when the complete implementation was generated elsewhere.

Secret of the States

Reading a program with the ability to envisage in one's head what is transpiring appears daunting to the novice programmer. However, a simple yet often overlooked skill is to consider the state rather than the execution. A step of execution will usually change the data associated with a program. A programmer is concerned with these changes only, and it is these state changes that provide a series of clues as to where an error occurs during execution.

State changes occur at several levels of abstraction, depending on what type of programming language is used. If a programmer is writing at the machine, or near machine (assembler), level, then state changes are clearly available for scrutiny. If a programmer is writing in a high-level compiled language (such as C or C++) then two distinct sets of state are available: (1) state created and manipulated within the program code itself; (2) state that is a result of compilation and represented natively on the hardware.

In this article, we are only concerned with compiled languages: a language that requires a compiler to turn all human/machine-written program code (source code) into machine-understandable program code (machine code) before any execution may occur. For such languages, modern programming environments provide extensive tools and techniques for “watching” the state during execution. This “debug” mode allows a programmer to specify precisely which state changes to track and provides the ability to drill down to actual assembler-level execution to view the values associated with memory locations. In the early days, a programmer would be restricted to manually displaying memory contents during execution using additional program code. In the modern day, any programmer exhibiting such behaviour should be viewed as not qualified to participate in the profession. Hopefully, after such programmers have been shown the errors of their ways, they may redeem themselves.

Languages that are either compiled for execution on a non-hardware execution environment (running on a language-dependent virtual machine – e.g., Java) or executed from source code before compilation has finished (scripting languages – e.g., Python) are not considered here. Such languages have their place and usually find favour in rapid development environments of non-substantial solutions (e.g., scripting) or where efficient solutions are not critical (e.g., Java). Minecraft is one of the few programs written in such languages (Java) that demonstrate such languages have their place and can be put to good use.

Programming Insight (AI) — State-First Debugging
  • AI proposes watch lists and conditional breakpoints.
  • Paste traces for AI interpretation; re-check in debugger.
  • Generate scripted lldb/gdb helpers via AI prompts.

Testing is not evaluating.

When considering R3, the novice programmer will assume we are discussing the notion of “testing”. This, in part, is correct, as we are indeed testing a solution to determine its correctness. However, the whole answer is not in the testing alone, but the actual evaluation of the solution. Ultimately, we evaluate a solution and then say it is either correct or not correct.

In the software industry, a company that wants to regard itself as approaching a professional standard will have the notion of unit testing emblazoned within its programming practices (be wary of companies that do not have such tests). Unit tests can be written in program code and can evaluate the data produced by a program under scrutiny. The more automated unit tests become, the more frequently program code can be assessed for correctness without slow, error-prone human involvement. This speeds up the development cycle and brings a basic standard of correctness across all program code within an organisation.

This brings about the one question that causes most arguments in a development environment:

“If a piece of program code passes all unit tests, then may it be considered correct?”

The lazy programmer will almost certainly answer “yes” to the above question. In fact, the lazy, stressed-out programmer who doesn’t fully understand all aspects of their program code will work towards passing the unit tests and then breathe a sigh of relief as the pain ends and their program finally passes all unit tests. This means the program code has passed the unit tests; NOT that such program code has been evaluated as correct.

Programming Insight (AI) — From Tests to Evaluation
  • Expand unit tests into concurrency and failure scenarios.
  • Generate metamorphic tests when exact outputs are tricky.
  • Ask AI to highlight coverage gaps and missing edges.

The importance of being human

Unit tests are valuable and essential, and one should always automate a decent testing strategy for their program code (e.g., pseudo clients/servers exploring all permutations in the state transition of a protocol). However, once all unit tests have been passed, the programmer MUST start the serious work of evaluation. This requires a programmer to manually read the program code and run such code through the debugger while watching the states change over time. Only the programmer can carry out this last step of evaluation, as only the programmer knows what their solution should be doing (as opposed to outputting, which others should be aware of). Programmers who are reluctant to do this or lack the ability to do this should be shown the door and returned to the classroom.

This last step of evaluation may appear onerous. However, considering the program code has passed the unit tests and such program code should not be lengthy (we test and evaluate small parts of an overall solution in isolation), the accomplished programmer should easily finish the evaluation in a matter of hours.

As an example, consider the following. A piece of program code is being evaluated that returns the largest integer from an input of 100 integers. The unit test always passes, as the output is always larger than any value inputted, with no erroneous behaviour evident. Unfortunately, sometimes a returned integer value may never have occurred in the original 100 integers at all! This would easily be spotted if a programmer had stepped through the program code and examined the state. Quite often, smaller data sets are all that is required to identify such problems. The real value of this manual approach lies in identifying serious flaws in the program code, such as memory leaks (where a program continuously allocates memory but fails to free it). Such errors lie in wait and tend to cause a failure at what appears to be random points in execution. The “100 integer” example given here would indicate a symptom relating to a more serious error of this nature.

Evaluating debug and release

As a final consideration, a programmer should always apply unit tests using the “release” version code and manually examine their code using the “debug” version code. The compiler usually has a flag that can be set to indicate if a compiled program will be debugged or not. If a compiler is set to “debug”, then extra program code is automatically generated. This additional program code allows the programmer to use the appropriate tools for debugging (e.g., watching variables, stepping through code).  This is useful for the final manual evaluation.

When the “debug” flag is turned on, there is, unfortunately, a higher probability that errors may be hidden from the output. For example, an error may exist that allocates only half the memory that is actually required for a piece of information. When such memory is needed to store the desired information, the additional memory “next door” is overwritten. In release mode, such memory may well be other program code or essential pieces of data, causing an error to be soon followed by a failure. In debug mode, the automated debug helper program code may be overwritten. If such program code is not required (used) then such an error may not result in visible failure throughout the lifetime of execution during a unit test.

A programmer should spot erroneous behaviour if they are stepping through their code in debug mode (not requiring a failure). Alternatively, automated unit tests require the increased likelihood of failure provided by release code.

Programming Insight (AI) — Catching Heisenbugs
  • AI drafts release-mode test harnesses with hardened allocators.
  • Generate canaries and poisoners to expose memory errors.
  • Suggest allocator/tool combos to surface subtle bugs.

Efficiency

A computer programmer strives to bring performance and correctness to their program code via the use of their own skill and knowledge. An efficient programmer extends this notion by trying to bring performance and correctness to their program code with help from other skilled and knowledgeable programmers. This does not mean wasting other people's time by getting them to help create program code; instead, this means exploiting program code that already exists. This can be in the form of already compiled code, provisioning programming tools (e.g., debug tools such as purify), existing program code that is correct and well written (e.g., library code provided with a commercial compiler), or simply executable program code required to create a suitable solution (e.g., device drivers allowing the use of specialist hardware).

Programming Insight (AI) — Build on Giants
  • AI surveys existing libraries against your constraints.
  • Draft adapter layers to integrate them safely.
  • Generate dependency configs (Conan/vcpkg) for you.

This brings about a further rule for the consideration of a computer programmer ……

  • Rule 4. Utilise existing program code and expertise

 

The programmer’s toolbox

To become a competent programmer, one must first become skilled in using existing programming tools and creating their own programming tools if the situation demands. Without mastery of at least the most common programming tools, someone pretending to be a computer programmer takes ten times longer to produce program code than those who are actually real computer programmers. At the very least, the novice programmer needs to become competent in tools that afford:

  • Software versioning – programmers spend much more time altering existing solutions than they do creating “new code”. If errors are introduced, then software versioning allows previous versions of a solution to be reinstated. In addition, if more than one programmer is working on a solution, or multiple solutions from different programmers are to be combined, then software versioning eases such styles of development: keeping track of who made changes to a solution and when (not to proportion blame, but to aid in assigning future tasks – putting effort into blaming others is fruitless).
  • Integrated Development Environment (IDE) – in addition to debugging tools (which we discussed at length in the pervious section) an IDE provides subtle help to the programmer: highlighting text based on syntax and hinting at foolish errors before compilation; auto-completing lines of program code in a suggestive, yet helpful, manner; hotkey and menu access to compiler commands easing development; integrated software versioning tools; help menus describing how one uses the IDE itself! In addition, some IDEs allow full access to language documentation, providing the programmer with no excuse for not knowing the syntax and grammar of a programming language.
  • Debugger – There is no shame in acknowledging that program code, at least during development, contains errors. In fact, the computer programmer must assume program code has errors within it, even if such errors do not make themselves visible during development. Errors must be tracked down and eliminated.

There are other tools and resources available, from code documentation generators through to optimisation profilers for determining the efficiency of a solution on particular platforms. For this article, we do not go into further details regarding such tools and techniques. However, the computer programmer should persistently look forward to learning how to use valuable tools as and when they appear. Those novice amateur computer programmers who shun such tools and think they can do better are misinformed.

Programming Insight (AI) — Daily Stack for C++
  • Pair IDEs with AI assistants (Copilot, Cursor, JetBrains AI).
  • Generate clang-tidy/CI configs automatically.
  • Draft benchmark harnesses with fixtures and ranges.

Colleagues and friends

In the commercial world of software development, computer programmers collaborate. They do not work in isolation, they do not compete with colleague, and they do not ignore requests for help from their colleagues. A team should work more effectively than a collection of single individuals. This is how the commercial world decides on the employment prospects of a new employee who considers himself or herself a computer programmer:

  1. Suppose a computer programmer is added to a team, and the overall output from that team goes up significantly (giving sufficient time to the new computer programmer to settle in). In that case, the computer programmer is probably good, so keep them.
  2. If a computer programmer is added to a team and the overall output from that team goes up, but not by much, (giving sufficient time to the new computer programmer to settle in), then the computer programmer is OK (give them a little longer to see if (1) happens, if it doesn’t, fire them)
  3. If a computer programmer is added to a team and the overall output from that team does not change or decreases (giving sufficient time to the new computer programmer to settle in), then fire them.

Just because a computer programmer has worked for a period of time in the commercial world (and collected a salary) does not necessarily indicate that they are competent. A new employee could find themselves in (1) above, but that may be because all other members of the team are hopeless. The above decisions are based on relevant comparisons of available knowledge (i.e., the existing computer programmers), not benchmarks of a known standard. However, over the long term, one can see that salary is indicative of quality. An assumption is made that a collection of amateur novice programmers (if at the same company) are paid less than a collection of professional computer programmers (if at the same company). For example, comparing the salaries of computer programmers at Google and “another company – not appropriate to say which, but we can guess” makes this clear.

Programming Insight (AI) — Team Acceleration
  • AI creates PR summaries and risk checklists.
  • Generate design docs from diffs for review.
  • Run repo-wide tidy/autofixes with AI scripting help.

Career

Working in the software industry is a job just like any other. However, because computer programmers are valuable and they create artefacts that can become valuable, there is a need to be able to understand what salary is appropriate. Furthermore, computer programming is a skill that one may not want to be indulging in later in life due to the constant battle to keep up to date with the latest trends in development platforms (something enjoyable in youth may not be so pleasant in old age).

Considering a career in the overall context of the professional computer programmer brings about another two rules:

  • Rule 5. Realise value and worth
  • Rule 6. Be capable of spotting and nurturing fellow computer programmers
Programming Insight (AI) — Measuring Value, Not Hours
  • AI helps turn logs into quarterly impact notes.
  • Practice coding tests/interviews in your stack with AI.
  • Auto-draft portfolio write-ups from your commits.

Moving job

Always remember, regardless of a computer programmer’s competency, an employer pays for an employee. The notion that “overtime”, “loyalty” and “above and beyond the call of duty” are unpaid, but beneficial employee traits to exhibit, is nonsense. The computer programmer is rewarded based on the outcome of the three possible decisions described in this article under the title “Colleagues and friends”. Basically, employers will have no hesitation in firing poorly performing computer programmers, regardless of how much time they hang around the offices or how loyal they are to the cause. However, if an employer does fire a competent computer programmer, then it indicates their ineptness, and such businesses will usually fail or persistently underpay staff. Therefore, the fired employee can’t really lose:

  • They are incompetent (at least compared to the standard of computer programmers at the company) and would be best served by leaving the profession (which they probably don’t like) or getting a computer programmer job elsewhere with fellow, poorer-performing programmers.
  • They are competent managers who have been incorrectly fired and are now free to get a better job with more competent programmers (probably for more money).

Master management

No matter how enjoyable programming may be now, there will come a point where one will want to leave the actual programming to others. Software and hardware rapidly change from one year to the next, and it is a constant race to keep up to date. This is what makes the subject fun and interesting, but as one gets older, other worldly pursuits should become more enjoyable. Therefore, management at later stages of a career is a natural step for the computer programmer. However, management is much more difficult than actually programming a computer because computer programmers are not as easy to “program” as a computer (so to speak). The most basic skills required to manage successfully are quite simple:

  • Hire computer programmers who can program appropriately (they should at least be capable of following the rules described in this article).
  • Facilitate an environment that fosters successful software production for a team of computer programmers.
  • Understand how much it costs, financially and in terms of time, to attain solutions suitable for satisfying the needs of the company.
  • Realise economies of scale in the software production pipeline to afford appropriate solutions while remaining practically within budget.

Points (3) and (4) appear very similar; however, (3) is concerned with judging how much it may cost to do something, whereas (4) is concerned with managing a constantly changing environment to do something within budget.

 And finally

Traditional first lessons in computer programming focus on writing program code, commenting such code, and running such code for a specific reason/purpose. Typical introductory lessons to programming present a student with a series of problems, which are then shown to be solvable by a piece of program code. Classic examples of such an approach would be: printing something to the screen (“hello world”); sorting numbers (“insertion sort”); inserting and deleting elements in a list (“pointers”); returning averages from a set of numbers (“numerical calculations”). The emphasis of such teachings is on how to structure program code to afford a solution. The student is easily confused into thinking that they are learning how to program by creating such examples. They are not. They are learning how to develop such examples.

Systems capable of generating such examples do not remove the educational purpose of learning C++; they sharpen it. Code is becoming cheaper to produce, but correct computational understanding and engineering judgement are not. The student must learn enough from each example to describe the computation, question its architecture, recognise inappropriate behaviour and decide what evidence would justify accepting the solution.

This brings us to our final rule (and, arguably, the most important one)……

  • Rule 7. Ensure that gaining an appropriate solution is a by-product of an enjoyable journey

When learning a programming language, the fundamental mistake is for the student programmer to work hard and focus completely on achieving a solution. The examples and exercises are there to encourage exploration of the programming language, the computer and the nature of programming a computer itself. This journey has scenery that needs to be observed, understood, and enjoyed. Getting to an appropriate solution should be tinged with sadness as one journey ends, and tinged with a touch of excitement as another problem appears and affords a new journey of discovery. The journey, the actual act of discovering new aspects surrounding the art of computer programming, should be enjoyable for the student programmer. If it is not, why bother?

Programming Insight (AI) — Learning With Joy
  • Ask AI for three solution paths, implement two, and compare.
  • Keep a “surprises log” auto-formatted by AI.
  • Build a daily loop: one prompt, one test, one reflection.

The rules again

  • R1. Be capable of estimating the timeliness of your computation
  • R2. Understand what a program is actually doing in a computational sense
  • R3. Use automated and human based approaches to evaluate correctness
  • R4. Utilise existing program code and expertise
  • R5. Realise value and worth
  • R6. Be capable of spotting and nurturing fellow computer programmers
  • R7. Ensure that gaining an appropriate solution is a by-product of an enjoyable journey
Programming Insight (AI) — The Rules, AI-Augmented
  • R1: AI estimates complexity & drafts benchmarks; you validate.
  • R2: AI describes invariants/state diagrams; you confirm.
  • R3: AI scaffolds tests/fuzzers; you judge correctness.
  • R4: AI surveys libs and writes adapters; you check ABI/licensing.
  • R5: AI drafts impact summaries; you negotiate with evidence.
  • R6: AI spots mentors/mentees; you nurture them.
  • R7: AI explores paths so the journey stays enjoyable.

Lesson 1 extension · the seven rules at system scale

What makes a program appropriate?

The seven rules begin with the programmer, but their consequences are visible in the program as a whole. Writing precise C++ is necessary. It does not, by itself, show that the result is appropriate. The deciding work is to determine where state should live, which component may change it, what assumptions cross an interface, how failure is contained, what evidence supports correctness, and which costs matter in the real workload.

Generated implementation makes this distinction impossible to ignore. A student may obtain source code without understanding the state it represents, the conditions under which it fails or the cost of running it at the required scale. Learning C++ gives the student the language and computational knowledge needed to expose those matters, inspect the proposed implementation and reject an attractive answer when it is inappropriate. Producing code is becoming easier. Judging the computation is not.

These are the first architectural decisions. Architecture begins whenever a choice affects several parts of a program or will be expensive to reverse. A shared global variable permits any dependent code to alter the state; an object with a controlled interface can restrict those changes. A resource with no clear owner can outlive its use or be released twice. A frame-time aspiration cannot tell us whether a measured result is acceptable; a stated budget can. The diagram comes later. The decision and its consequences come first.

A working test for an appropriate program

For this course, an appropriate program satisfies explicit requirements, maintains its invariants for the inputs and failures it claims to handle, makes important responsibilities visible in its interfaces, and supplies proportionate evidence that it behaves correctly within its resource budgets. Another programmer must also be able to understand and change it without relying on luck.

The screen says 42. Good. What does this prove? It proves that this build finished and that one path produced 42 from one input. It says nothing yet about an empty file, a negative score, a workload ten times larger or a file handle that was never released. Those cases matter because a successful demonstration is an event, not an argument. The argument connects the requirement to the behaviour and explains why the evidence is sufficient.

DimensionQuestion an architect asksPossible evidence
FitnessWhich stated requirement does this behaviour satisfy?Acceptance examples and traceability from requirement to code
CorrectnessWhich states are valid, and what must remain true?Invariants, tests, assertions, analysis and review
StructureWhich component owns this decision and which details are hidden?Small explicit interfaces and dependency boundaries
EfficiencyWhat workload and resource budget matter?Complexity reasoning, profiles and controlled measurements
ChangeabilityWhat is likely to change, and how far will that change spread?Cohesive components, low coupling and focused tests
OperabilityHow will failure be detected and diagnosed?Diagnostics, logs, metrics and reproducible failure cases

Ask the question before choosing the class

A class can make a design look settled while the problem remains vague. Before I write one, or ask an AI system to propose one, I write down answers to five questions:

  1. What must be true? Turn vague wishes into observable requirements and explicit constraints.
  2. What state exists? Identify the values that describe the system and the valid transitions between them.
  3. Who is responsible? Give each decision, invariant and resource a clear owner.
  4. What may vary? Hide volatile details behind an interface while keeping stable policy visible.
  5. What would count as evidence? Decide how correctness and resource use will be demonstrated before implementation makes the convenient cases look persuasive.

The result is a feedback cycle rather than a one-way march:

requirement → model of state → interface and ownership → implementation → evidence → revised understanding

Finding an ambiguous requirement, a missing invariant or a failed performance assumption is useful evidence. It tells us that the model must change. Hiding that result and continuing to build preserves the schedule for a moment, but it also preserves the defect.

This cycle separates directing computation from merely requesting code. A natural-language instruction, whether given to a colleague or to an AI system, does not supply the missing requirement, state model or ownership decision. The implementation is a proposal. Accept it only when its behaviour can be connected to the model, its assumptions can be challenged and its important claims have evidence.

Worked development: health as controlled state

Suppose I am reviewing a small game. Health must start between 0 and 100. Damage must never increase it or take it below zero, and other code needs to ask whether the player is alive. If I call this “just an integer”, every assignment can appear harmless. I prefer to see controlled state with a rule that must survive every operation:

Invariant: while a PlayerHealth object is valid, 0 <= points <= 100.

This complete example compiles as C++20. I have introduced some class syntax before its formal lesson, so do not worry if every mark is not yet familiar. Read the public names as promises made to the rest of the program. Then notice the important restriction: arbitrary code cannot assign an invalid number directly to points_.

#include <algorithm>
#include <cassert>
#include <iostream>
#include <stdexcept>

class PlayerHealth {
public:
    static constexpr int maximum = 100;

    explicit PlayerHealth(int initial) : points_{initial}
    {
        if (initial < 0 || initial > maximum) {
            throw std::out_of_range{"health must be between 0 and 100"};
        }
    }

    [[nodiscard]] int points() const noexcept { return points_; }
    [[nodiscard]] bool alive() const noexcept { return points_ > 0; }

    void take_damage(int amount)
    {
        if (amount < 0) {
            throw std::invalid_argument{"damage cannot be negative"};
        }
        points_ = std::max(0, points_ - amount);
    }

private:
    int points_;
};

int main()
{
    PlayerHealth health{10};
    std::cout << "start: " << health.points() << '\n';

    health.take_damage(3);
    assert(health.points() == 7);
    std::cout << "after 3 damage: " << health.points() << '\n';

    health.take_damage(20);
    assert(health.points() == 0);
    assert(!health.alive());
    std::cout << "after 20 damage: " << health.points() << '\n';
}

Expected output:

start: 10
after 3 damage: 7
after 20 damage: 0
EventState beforeCheck or transitionState afterInvariant
Construct with 10No object10 is within the permitted range10, alivePreserved
take_damage(3)10, alive3 is valid; subtract and clamp at zero7, alivePreserved
take_damage(20)7, alive20 is valid; result is clamped at zero0, not alivePreserved
take_damage(-2)Any valid stateReject invalid request before mutationUnchangedPreserved

Why this is architecture rather than decorative syntax

  • The invariant has one owner. Callers request a transition instead of rewriting representation.
  • The interface uses the language of the requirement: take_damage, points and alive. Incidental arithmetic is not exposed.
  • An invalid request fails at the boundary, close to its cause, instead of allowing corrupt state to travel through the program.
  • The implementation could later change its representation without forcing every caller to change, provided the contract remains stable.
  • The assertions check important observations, but they do not prove the whole class correct. Boundary values, invalid construction and interaction with the larger game still require evidence.

The interface centralises the policy, which also requires every health-changing operation to pass through it. That restriction is useful because all of those operations must preserve the same invariant. If the data had no shared rule, the class would add ceremony without protecting anything. A boundary is architectural when its restriction has a reason.

Efficiency: growth and budget

Rule 1 becomes more useful when two different questions are kept separate:

  1. Growth: how does the amount of work or storage change as the problem grows?
  2. Budget: on the target system and realistic workload, does the observed cost fit the requirement?

take_damage performs a fixed number of comparisons and arithmetic operations and performs no dynamic allocation. Its work therefore does not grow with the number of players or items elsewhere in the game. That conclusion follows from the operation itself, but it does not promise a particular number of nanoseconds. Compiler optimisation, surrounding code, hardware, instrumentation and contention can alter the observed time. If the operation sits inside a critical loop, measure that loop in a representative build and workload. The source explains growth; the measurement decides whether the budget is met.

Precision note: P, NP and randomness

The familiar practical description is that problems in P can be solved in polynomial time by a deterministic algorithm, while problems in NP have proposed solutions that can be verified in polynomial time. The formal definition of NP can also be expressed using a nondeterministic abstract machine. Here, “nondeterministic” is a mathematical model; it does not mean repeatedly making random guesses and hoping to become correct.

Sorting is already in P. Randomly swapping values until they happen to be ordered is therefore not an example that distinguishes P from NP; it is merely a poor randomized procedure for a problem with well-known deterministic polynomial-time algorithms. Randomized algorithms form their own area of analysis, with explicit statements about probability, expected cost and error. Keeping these meanings separate prevents a useful concern with timeliness from becoming an inaccurate complexity claim.

Do not optimise a feeling. State the budget, identify the path that threatens it and then measure:

QuestionWeak answerEngineering answer
Is it fast?“It looked instant.”“At the 99th percentile, this workload remains within the stated frame budget on the target hardware.”
Is this algorithm efficient?“It uses clever low-level code.”“Its growth matches the expected input range, and profiling shows this path matters.”
Should we optimise it?“The code could be shorter/faster.”“A measured bottleneck threatens a requirement, and the proposed change can be compared without weakening correctness.”

A stable interface can leave room for a different representation, batching strategy or algorithm without spreading the experiment through unrelated code. This does not make the first design fast. It makes a measured change less expensive to contain.

How much of a dependency must you understand?

Rule 4 cannot require every programmer to reimplement every library or audit every line in a compiler, operating system and graphics driver. No one could complete a modern system on that basis. The required depth of understanding is decided by the dependency, the claim made about it and the consequence if that claim is wrong.

For a third-party component, record the contract on which the program relies, its version and provenance, its ownership and lifetime rules, its error behaviour, relevant thread-safety assumptions, performance-sensitive operations, licensing constraints, and the evidence used to accept it. Test the behaviour your program actually uses. If replacement or failure would spread through the system, contain the vendor-specific details behind a narrow adapter. High-consequence code demands stronger assurance than a disposable tool because the cost of an incorrect assumption is greater.

Abstraction and accountability coexist when the interface states what is being trusted and the evidence is proportionate to the risk. Reimplementing the component may simply replace a known dependency with an untested one.

Generated implementation has the same obligation. Fluent explanation and plausible-looking source do not establish provenance or correctness. The machine may prefer a representation that is difficult for a human to inspect, so retain human-readable views of the computation where they help: interfaces, invariants, state-transition tables, execution traces, architectural descriptions and selected C++ fragments. These expose what was built, where responsibility lies and which behaviour still needs to be challenged.

Evidence is a portfolio, not a single test

Testing and evaluation are not synonyms. A test supplies an observation under stated conditions. Evaluation decides what that observation, together with the other available evidence, justifies us in claiming. Different techniques expose different mistakes:

  • Compiler diagnostics reject ill-formed code and warn about suspicious constructs, but successful compilation does not establish the intended requirement.
  • Focused tests demonstrate selected examples and boundaries, but only for the cases and observations encoded in them.
  • Property and fuzz testing explore broader input spaces, but their value depends on the property or oracle being correct.
  • Static and dynamic analysis can expose lifetime, bounds, race and undefined-behaviour risks, but each tool has a defined coverage boundary.
  • Human review can challenge requirements, architecture and readability, but reviewers need context and can overlook detail.
  • Profiling and benchmarking measure resource behaviour for a workload and environment; they do not prove semantic correctness or universal speed.
  • Operational observation reveals real workloads and failures, but arriving in production is not a substitute for pre-release reasoning.

The resulting claim must retain its boundary: “this evidence supports this property under these conditions.” Remove the property or the conditions and the evidence no longer supports the same conclusion.

Debug and release: language guarantee versus observation

C++ describes the observable behaviour of an abstract machine. A conforming implementation may transform the program as long as the required observable behaviour is preserved. This is commonly called the “as-if” rule. Debug and optimised builds can consequently expose different timing, layout and diagnostic experiences without representing two different C++ languages.

However, if execution reaches undefined behaviour, the C++ standard no longer constrains the result in the normal way. A debug build appearing to work is then weak evidence, not a contract. Architecture helps by containing unsafe operations, making invariants explicit and keeping ownership and lifetime relationships reviewable. Tooling helps by enabling warnings and appropriate analysers or sanitizers. Neither removes the need to understand the rule the program depends upon.

The seven rules as system-level questions

RuleArchitectural readingQuestion for a design review
R1 · TimelinessMake resource budgets and workload growth explicit.Which path threatens which measurable budget?
R2 · Computational understandingModel state, transitions, ownership and failure boundaries.Where can invalid state enter, and who prevents it?
R3 · EvaluationConstruct an evidence portfolio around requirements and risks.Which claims does each piece of evidence support, and where are its limits?
R4 · Existing expertiseUse dependencies behind understood, proportionate contracts.Which assumptions bind us to this component?
R5 · ValueOptimise for useful system outcomes and sustainable change.Which engineering cost or user need does this decision address?
R6 · Nurture programmersMake decisions teachable through names, interfaces, reviews and rationale.Can another programmer safely extend this without reading our minds?
R7 · Enjoy the journeyTreat experiments and revision as learning, not embarrassment.What did this result teach us about the model?

Judge the effect, not the activity

Lines of code, hours in the office, salary and visible activity are easy to count. They are also weak evidence of a programmer's effect because the result depends upon task selection, the existing system, tools, mentoring and the team around the work. Removing a recurring failure may delete code. Clarifying an interface may save work that is never seen. Making a build reproducible may allow every other programmer to proceed. Judge the consequence, not the noise surrounding it.

Architecture is partly a communication discipline for this reason. A decision record preserves why a boundary exists. A name preserves domain meaning. A focused review distributes understanding. An automated check prevents an agreement from being eroded accidentally. If only the author can modify a design safely, the system contains a single point of human failure, however clever the implementation appears.

Generated implementation weakens code volume as a measure still further. The programmer's work includes defining the problem, modelling the computation, choosing boundaries, stating constraints, directing implementation, inspecting the result, debugging failures and judging the evidence. Calling this prompt engineering misses the difficult part. The programmer must understand enough to reject a convincing implementation that solves the wrong problem or hides an unacceptable risk.

Programming Insight (AI): Architecture review with evidence

Directing an AI-generated implementation is an engineering activity, not a prompt-writing shortcut. Use AI as a proposal and challenge tool within a process whose requirement, invariant, constraints, ownership model and target workload remain explicit and human-understood.

  1. Ask it to list hidden assumptions, alternative boundaries and failure cases. A request to “improve” the code is too vague.
  2. Require it to describe the state, permitted transitions, ownership and failure behaviour represented by its proposal, then check that account against the implementation.
  3. Require every recommendation to state the property it is intended to improve and the trade-off it introduces.
  4. Compile proposed C++ with the course warnings, run the relevant tests, and check language claims against authoritative documentation.
  5. Measure performance claims in the actual workload. Reject confident explanations that arrive without evidence.
  6. Record the decision in your own words. If you cannot defend it without the conversation, you do not yet own the design.

Review the architecture before coding

  • What observable requirement or risk justifies this component?
  • What state does it own, and what invariant defines valid state?
  • Which operations form its smallest useful interface?
  • Which dependencies cross the boundary, and why?
  • Who owns each resource, and when does that ownership end?
  • Which failures are rejected, represented, recovered from or allowed to propagate?
  • What workload and resource budget matter?
  • What evidence will show correctness, and what evidence will show efficiency?
  • Which likely change would be hardest to accommodate?
  • Could another programmer explain and safely modify the design?

How this perspective develops through the course

Lesson 2 begins the detailed model of state. Lesson 4 and Lesson 7 develop callable and source-level boundaries. Lessons 1416 make aliasing, ownership and lifetime explicit. Lesson 17 connects representation to locality and measured cost. Lessons 1822 examine object boundaries, relationships and interface behaviour. Lesson 24 consolidates deterministic cleanup and special-member design. Each subject gives the student another human-readable way to understand and supervise computation. The syntax changes; the architectural questions remain.

Standards and engineering evidence used here