C++ Part I · Module 1 · Why C++ Matters in Semiconductor Engineering
How a C++ Program Becomes a Running Program
Every chapter so far has used the word compile without accounting for it. This is the debt being paid, and it is the last orientation this module owes you — because the model built here is one that four later modules lean on directly. The story is not really about four stages with four flags; Module 2 does that properly, with the intermediate files open on screen. The thing worth establishing first is the shape: a real program is many source files, each compiled independently into its own object file, with a linker afterwards resolving the names between them. One consequence of that shape explains more C++ behaviour than any other single fact in this module — the compiler sees exactly one translation unit at a time, and knows nothing whatsoever about the others. We will prove it by compiling a file containing a declaration that is simply wrong, watch it succeed without a single diagnostic, and then watch two different languages respond very differently when the pieces are put together.
Foundation18 min readC++Part IToolchainLinking
Part I · Module 1 · Chapter 1.7 · How a C++ Program Becomes a Running Program
1. One File Is Not the Interesting Case
A single-file program has a simple story: you hand the compiler main.cpp, it hands you a runnable file, and nothing needs explaining.
Real programs are not one file. A reference model, an architectural model, an EDA utility — any of the things in Chapters 1.5 and 1.6 — is tens to thousands of source files, and the way those files become one program is the subject of this chapter. It matters for a specific reason: almost every confusing C++ error message, and the entire existence of header files, is a consequence of the structure described here. An engineer who holds this model reads those errors; an engineer who does not guesses at them.
Chapter 1.1 §2 said source code is a description and a program is what runs. Now we fill in what happens in between, at the level of structure rather than mechanics.
2. The Shape
Here is the pipeline. Read it as the shape of the process, not as a list of commands you are expected to type — Module 2 gives you the commands and shows you each intermediate file.
Two words in that figure carry the weight.
Translation unit is the unit the compiler actually processes: one source file plus everything its #include directives pulled in. This is a real term from the standard, not jargon. When a later chapter says "the compiler knows nothing about that", the unit of not-knowing is the translation unit. Module 24.1 defines it formally.
Unresolved describes what an object file still contains. It holds finished machine code for the functions defined in its own translation unit, and for every function it calls but does not define, it records a name and the fact that something else must supply it. An object file is therefore not runnable, and that is not a defect — it is the whole mechanism by which separate compilation works.
3. Watch It Happen
Three files. A header declaring a function, a source file defining it, and a program using it.
#ifndef CHECKSUM_HPP
#define CHECKSUM_HPP
#include <cstddef>
#include <cstdint>
std::uint32_t checksum(const std::uint8_t* data, std::size_t len);
#endif#include "checksum.hpp"
std::uint32_t checksum(const std::uint8_t* data, std::size_t len)
{
std::uint32_t sum = 0;
for (std::size_t i = 0; i < len; ++i) { sum += data[i]; }
return sum;
}#include "checksum.hpp"
#include <iostream>
int main()
{
const std::uint8_t payload[4] = {0x10, 0x20, 0x30, 0x40};
std::cout << "checksum = " << checksum(payload, sizeof payload) << '\n';
}Compile each source file separately — -c means "compile but do not link" — then link the results:
clang++ -std=c++17 -Wall -Wextra -Wpedantic -c main.cpp -o main.o
clang++ -std=c++17 -Wall -Wextra -Wpedantic -c checksum.cpp -o checksum.o
clang++ main.o checksum.o -o app
./appOn the machine used for this chapter:
main.o 10,336 bytes
checksum.o 616 bytes
app 38,656 bytes
checksum = 160The output is right: 0x10 + 0x20 + 0x30 + 0x40 is 160.
The size asymmetry is worth a moment. checksum.o is 616 bytes — it contains one short loop. main.o is sixteen times larger, and main is three lines long. The difference is <iostream>: using the output stream pulled in a substantial amount of machinery, and that machinery landed in the translation unit that asked for it. Nothing about checksum.cpp changed, because checksum.cpp never knew <iostream> existed. That is separate compilation visible as a number.
Notice also what the link command does not have: no -std=c++17, no warning flags. Those are instructions to the compiler, and the compiler has finished. The linker is a different tool with a different job.
4. The Linker Works on Names
Open the object files and look. nm lists the symbols in an object file; -u asks for the ones it does not define.
nm -u main.o
__Z8checksumPKhm ← needs this from somewhere
__ZNKSt3__18ios_base6getlocEv
... (more, all from <iostream>)
nm -gU checksum.o
0000000000000000 T __Z8checksumPKhm ← provides exactly thatmain.o needs __Z8checksumPKhm. checksum.o provides it. The T means the symbol is defined in the code section. The linker's entire job, reduced to one sentence, is to make those two lists cancel out.
That name is not a typo. C++ encodes a function's parameter types into its symbol name — a process called name mangling — and the encoding can be read back:
nm -gU checksum.o | awk '{print $NF}' | c++filt
checksum(unsigned char const*, unsigned long)The symbol carries the full signature. Remember that; §5 turns it into the chapter's main point.
And if the linker cannot make the lists cancel, it says so:
clang++ main.o -o app
Undefined symbols for architecture arm64:
"checksum(unsigned char const*, unsigned long)", referenced from:
_main in main.o
ld: symbol(s) not found for architecture arm64Read the shape of that message, because it is a shape you will see for the rest of your career:
- No line number, and no source file. The linker never saw your source. It cannot tell you where in
main.cppthe call was written. - It names a symbol, not a statement.
- It says which object file referenced it — that is the level it works at.
ldis the linker's own name in its own message.
A compiler error carries a file, a line and a column. A linker error carries a symbol and no line. Module 2.7 turns that distinction into a debugging routine; for now it is enough to know they are different tools failing at different times.
5. The Compiler Sees One Translation Unit At a Time
This is the sentence to take away from the chapter, and it is worth proving rather than asserting.
Here is a file that declares checksum incorrectly. The real function takes a pointer to bytes; this declaration claims it takes a pointer to 32-bit words.
#include <cstdint>
#include <cstddef>
// A declaration that does NOT match the definition in checksum.cpp:
// it claims checksum takes a pointer to 32-bit words, not to bytes.
std::uint32_t checksum(const std::uint32_t* data, std::size_t len);
int main()
{
const std::uint32_t payload[1] = {0x40302010u};
return static_cast<int>(checksum(payload, 1));
}Compile it on its own:
clang++ -std=c++17 -Wall -Wextra -Wpedantic -c wrongdecl.cpp -o wrongdecl.o
→ exit 0, no diagnosticsNothing. Not an error, not a warning, at the strongest warning level this curriculum uses. And the compiler is behaving correctly: within this translation unit the program is impeccable. A function was declared, the call matches the declaration perfectly, and the declaration is a promise the compiler is entitled to believe. The definition lives in a different translation unit that this compilation never saw and could not have seen.
Now look at what it asked for, and compare:
wrongdecl.o asks for __Z8checksumPKjm → checksum(unsigned int const*, unsigned long)
checksum.o provides __Z8checksumPKhm → checksum(unsigned char const*, unsigned long)PKj is "pointer to const unsigned int". PKh is "pointer to const unsigned char". The two names differ, so the lists cannot cancel:
clang++ wrongdecl.o checksum.o -o app
Undefined symbols for architecture arm64:
"checksum(unsigned int const*, unsigned long)", referenced from:
_main in wrongdecl.oThe mistake was caught — not by the compiler, which could not see it, but by the linker, because the parameter types were encoded in the name.
The same mistake in C
Now the contrast that makes the point land. The identical error, in C:
/* c_check.c — the definition, taking bytes */
uint32_t checksum_c(const uint8_t *data, size_t len) { /* ... */ }
/* c_main.c — the declaration, claiming words */
uint32_t checksum_c(const uint32_t *data, size_t len);C does not mangle. The symbol is just the name:
C symbol provided: _checksum_c
C symbol requested: _checksum_c ← the types are NOT in the nameThe two lists cancel. It links, with no diagnostic from any tool:
clang c_main.o c_check.o -o c_app && ./c_app
result = 16Sixteen. The correct answer for that payload is 160 — the program read one byte, 0x10, and stopped. Two translation units disagreed about a function's parameter types, every tool was satisfied, and the program ran and produced a plausible wrong number.
6. What the Model Explains
The payoff. Each of these is a question beginners ask, and all of them have the same answer.
Why do header files exist? Because the compiler sees one translation unit at a time, every unit that calls checksum must be told independently that it exists and what its signature is. Writing that declaration by hand in every file would mean many chances to write it differently — exactly the wrongdecl.cpp failure. A header is one copy of the declaration that every unit includes, so they cannot disagree. Module 24.2 and 24.3 make this a policy.
Why isn't a declaration enough? Because a declaration is a promise and the linker needs a definition. wrongdecl.cpp compiled because its promise was internally consistent; it failed to link because nothing kept the promise.
Why do I get two completely different styles of error? Because two different tools are checking two different things. The compiler checks one translation unit against the language's rules and reports a file and a line. The linker checks symbol names across all of them and reports a symbol. Module 2.7.
Why does changing a header rebuild so much? Because a header is pasted into every translation unit that includes it, so editing it changes every one of those units. This is why Module 24.4 cares about forward declarations — reducing what a header drags in reduces what a change to it rebuilds.
Why must a function be defined once and only once? Because two object files providing the same symbol give the linker two answers to a question that has one. That rule is the One Definition Rule, and Module 24.1 states it precisely, including the exceptions that make header-defined inline functions and templates legal.
7. Two Deliberate Simplifications
Chapter 1.3 §8 promised that every simplified model in this curriculum would be labelled when introduced. Two are in play here.
"Object file" is a toolchain concept, not a language one. The standard describes translation in terms of ordered phases and defines a translation unit. It does not require that a separate file of machine code exist at any point, does not require an assembly step, and does not name a linker. An implementation need only behave as if the required order had been followed; one is free to fuse the stages entirely. What §3 and §4 showed is what essentially every real toolchain does, and it is worth learning because each stage produces its own kind of error — but do not mistake it for a rule of the language.
The pipeline in Figure 1 is a shape, not a procedure. Module 2.5 and 2.6 walk the stages one at a time with the intermediate files open, which is where the mechanics belong. This chapter deliberately stopped at the structure, because the structure is what the rest of Part I needs.
One more honest note on the symbol names: __Z8checksumPKhm, nm, c++filt and the Mach-O object format are all platform specifics from one machine. The mangling scheme itself is not standardised — it is a property of a platform's application binary interface, which is precisely why object files from two different compilers generally cannot be linked together. The portable lesson is only this: C++ encodes signatures into symbol names, and C does not.
8. What You Should Be Able to Say Now
- A real program is many source files. Each becomes its own translation unit, then its own object file, and only the linker sees them together.
- A translation unit is one source file plus everything its
#includedirectives pulled in. It is the unit the compiler processes and the unit of what the compiler does not know. - An object file holds finished machine code plus a list of names it needs from elsewhere. It is not runnable, by design.
- Separate compilation is visible as a number:
main.owas 10,336 bytes because it used<iostream>;checksum.owas 616 because it never heard of it. - The linker's job is to make the "needs" and "provides" lists cancel.
main.oneeded__Z8checksumPKhm;checksum.oprovided it. - The compiler sees one translation unit at a time. A file containing a wrong declaration compiled with zero diagnostics at
-Wall -Wextra -Wpedantic, because within that unit nothing was wrong. - C++ mangles names, encoding parameter types into the symbol. That is why the mismatched declaration failed to link.
- C does not mangle. The same mismatch linked silently and printed
16where the answer was160. extern "C"trades that signature checking for C callability. It is a decision, not boilerplate — Module 24.5 for the mechanism, Module 28.6 for the DPI boundary.- Compiler errors carry a file and a line; linker errors carry a symbol and no line. Different tools, different checks, different times.
- Headers exist so that many translation units cannot disagree about one declaration.
- "Object file" and the four-stage arrangement are what toolchains do, not what the standard requires. The mangling scheme is a platform ABI property, not standardised.
9. Exercises
Exercise 1 — attribute the failure. For each, say whether the compiler or the linker reports it, and give a one-line reason: (a) a missing semicolon in checksum.cpp; (b) checksum.cpp was never compiled; (c) checksum is defined in two different .cpp files; (d) checksum.hpp could not be found; (e) the program builds and prints 16.
Exercise 2 — explain the size difference. main.o is roughly sixteen times the size of checksum.o, and main is three lines long. Explain why, and say what would happen to each file's size if checksum.cpp also included <iostream> but did not use it.
Exercise 3 — justify the silence. wrongdecl.cpp compiled with no diagnostics at the strongest warning level used in this curriculum. Explain why that is the compiler behaving correctly rather than a missed check, and name the single piece of information it did not have.
Exercise 4 — read the mangled names. Given that PKh means "pointer to const unsigned char" and PKj means "pointer to const unsigned int", say what you would expect the mangled name to contain if checksum's second parameter changed from std::size_t to int, and whether the existing main.o would still link against the new checksum.o.
Exercise 5 — argue both sides of the trade. A colleague marks every function in a C++ library extern "C" so that "anything can call it". Using §5, give the specific protection they have discarded, describe a concrete bug that can now reach a running program, and then name one situation in which their decision would nonetheless be correct.
Exercise 6 — defend the header. Explain, using §6, why writing std::uint32_t checksum(const std::uint8_t*, std::size_t); by hand at the top of each file that needs it is legal, builds correctly, and is still a defect waiting to happen. Name the specific failure and which tool would eventually catch it.
Exercise 7 — the C answer is worse, precisely. In §5, C produced 16 and C++ produced a link error. Explain why the link error is the better outcome in terms of when the mistake was found and what it would cost to find it the other way — and say what the symptom would have looked like to the engineer debugging the C version.
10. Summary
- Many in, one out. Many source files, each its own translation unit, each its own object file, one linker, one program.
- A translation unit is a source file plus its includes — the unit the compiler processes, and the boundary of what it knows.
- An object file is unfinished by design: machine code plus a list of names something else must supply.
- Separate compilation is measurable.
main.o10,336 bytes with<iostream>;checksum.o616 bytes without it. - The linker works on names. It makes "needs" and "provides" cancel, and its errors carry a symbol and no line number.
- The compiler sees one translation unit at a time. A wrong declaration compiled with zero diagnostics, correctly.
- C++ encodes signatures into symbol names. The mismatch failed to link. The same mismatch in C linked silently and printed 16 where the answer was 160.
extern "C"is a trade: C callability in exchange for that checking. Module 24.5 for the mechanism, Module 28.6 for the boundary where it is made.- This model explains headers, two error families, rebuild cost, and the One Definition Rule — all consequences of one-translation-unit-at-a-time.
- Two labelled simplifications: object files and the four-stage arrangement are toolchain practice, not language requirement; the mangling scheme is a platform ABI, not standardised.
Module 1 is finished, and it has deliberately taught you no C++ you can write. What it has given you is a destination for everything that follows: a language defined by a standard rather than by a compiler, with a version this curriculum holds itself to; a design rationale that makes lifetime and ownership the centre of the subject rather than an advanced topic; a map of where the language is actually used and which parts of it that work needs; and a build model that explains why a header exists and why an error arrives from the tool it does.
Module 2 starts where this chapter stopped. It takes the pipeline apart one stage at a time, with the intermediate files open on screen, and it ends with enough console output that every example from Module 3 onward is something you can run and observe rather than something you have to take on trust.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
