C++ / NAMESPACES, HEADERS, AND BUILDS
Headers versus source files in real projects
Decide what belongs in a .hpp versus a .cpp and use inline, extern, forward declarations, and unnamed namespaces to keep that split correct.
What you will learn
- Decide placement by asking whether another translation unit must see the entity
- Mark header-defined free functions inline and header variables extern
- Keep file-local helpers in an unnamed namespace inside the .cpp
- Swap an #include for a forward declaration when a pointer or reference suffices
Understanding Headers versus source files in real projects
A C++ compiler processes one translation unit at a time: a single .cpp file plus the full text of everything it includes, and it remembers nothing about the other files in the project. That is why headers exist at all. A header is the block of text you paste into every unit that needs to know a name, so its job is to say what exists, while the .cpp says what those things do. Declarations may be repeated in every unit without harm; definitions generally may not, because the finished program needs exactly one of each.
The split is therefore not interface versus code, it is whatever the compiler needs at the point of use versus everything else. Class definitions, enums, templates, and constexpr or inline functions must be visible to each caller, so they belong in the header; a free function you define in a header needs the inline keyword, which tells the linker that identical copies across units are one entity rather than a clash. Variables travel in the opposite direction: the header carries extern int hits; and exactly one .cpp carries the definition int hits = 0;.
Headers are expensive in two ways .cpp files are not. Every name in a header becomes something clients may depend on, and editing a header rebuilds everything that includes it. So push private things down into the .cpp: helpers into an unnamed namespace, includes that only the implementation needs, types nobody else names, and use a forward declaration in the header whenever a pointer or reference is all the declaration requires. The header is a contract you have to keep; the .cpp is a notebook you can rewrite without anyone noticing.
A header is text copied into every translation unit, so it holds declarations that may repeat, while the matching .cpp holds the single definition of each thing.
<iostream>
// ===== geometry.hpp : what exists =========================
namespace geometry {
double area(double w, double h); // declaration only
inline double perimeter(double w, double h) { // body in a header -> inline
return 2.0 * (w + h);
}
extern const char* const unit; // defined in exactly one .cpp
}
// ===== main.cpp : sees only the declarations above ========
int main() {
std::cout << "area = " << geometry::area(3.0, 4.0)
<< ' ' << geometry::unit << "^2\n";
std::cout << "perimeter = " << geometry::perimeter(3.0, 4.0)
<< ' ' << geometry::unit << '\n';
}
// ===== geometry.cpp : what it does ========================
namespace geometry {
const char* const unit = "m";
namespace { // private to this file
double fudge_factor() { return 1.0; }
}
double area(double w, double h) { return w * h * fudge_factor(); }
}
A header is text pasted into every translation unit, so it carries repeatable declarations while the matching .cpp carries the one definition.
Worked examples
Forward declaration instead of an include
Shows a header declaring an API against an incomplete type so the full type is only needed by the .cpp.
<iostream>
// ===== logger.hpp =====
class Report; // forward declaration, no members described
void print(const Report& r); // legal against an incomplete type
// ===== report.hpp =====
class Report {
public:
explicit Report(int n) : lines(n) {}
int lines;
};
// ===== logger.cpp : includes report.hpp because it touches members =====
void print(const Report& r) { std::cout << "lines: " << r.lines << '\n'; }
int main() {
Report r{3};
print(r);
}
Example explained
Line 1class Report; introduces the name without the layout, which is all logger.hpp needs.
Line 2A const Report& parameter needs no size and no members, so the declaration compiles fine.
Line 3r.lines does need the complete type, so report.hpp is included by logger.cpp, not by logger.hpp.
Line 4Adding a member to Report then rebuilds only files that include report.hpp, not every user of logger.hpp.
Class in the header, member bodies in the source
Shows why the class definition itself cannot leave the header even though its member bodies can.
<iostream>
// ===== counter.hpp =====
class Counter {
public:
explicit Counter(int start); // declared only
void bump(); // declared only
int value() const { return n_; } // in-class body: implicitly inline
private:
int n_; // clients need this for sizeof
};
// ===== counter.cpp =====
Counter::Counter(int start) : n_(start) {}
void Counter::bump() { ++n_; }
int main() {
Counter c{41};
c.bump();
std::cout << "value = " << c.value()
<< ", sizeof(Counter) = " << sizeof(Counter) << '\n';
}
Example explained
Line 1The class definition stays in the header because a caller writing Counter c{41}; must know its size and layout.
Line 2explicit Counter(int) and bump() are declarations, so rewriting their bodies recompiles only counter.cpp.
Line 3value() is defined inside the class, which makes it inline automatically and safe in a header included many times.
Line 4Counter::bump() in counter.cpp is the one definition every call in the program is linked against.
Important notes
inline is a linkage promise, not a performance request. It permits one definition per translation unit for the same entity and never obliges the compiler to expand anything at the call site.
The .hpp/.cpp naming is convention, not language. The compiler compiles the files you hand the driver and pastes the files you #include, so a header added to the build is compiled as its own translation unit, and an included .cpp behaves exactly like a header.
Common mistakes
Writing an ordinary function body in a header and forgetting inline: every including .cpp emits that symbol, so each file compiles but the link fails with a duplicate definition.
Writing int counter = 0; in a header instead of extern int counter;: either the linker reports the symbol twice, or, when the variable is const or static, each file silently gets its own copy and writes made in one file are invisible in the others.
Moving a template's body into the .cpp like a normal function: nothing instantiates it there, so callers link against a symbol that was never generated and you get undefined reference to f<int>.
Try it yourself
Change, predict, then run
In a browser editor, write one file split by comments into counters.hpp, main.cpp, and counters.cpp sections: the header declares extern int hits; and defines inline double rate(int, int), main uses both, and the implementation section defines hits plus a helper in an unnamed namespace. Then delete extern from the header line and read the redefinition error, which is exactly what the linker would report if two .cpp files included that header.
Open the C++ workspaceCheck your understanding
A header defines double clamp01(double) with a body and no inline. Three .cpp files include it. Each file compiles cleanly, but the link fails. What happened?
- Header files are never compiled, so clamp01 has no definition anywhere in the program.
- Only the first .cpp that includes the header keeps the body; the other two reference a symbol nobody defined.
- Each .cpp pasted the body in and emitted its own external definition, so the linker sees the same symbol three times.
- The compiler refuses to emit a function whose body it cannot expand at every call site.
Show answer
Each translation unit is compiled in isolation, so all three legitimately produce a definition of clamp01 and only the linker, which sees all three object files, can notice the collision; inline is what would let those copies collapse into one entity. Option 2 is tempting because include guards do stop repeated text, but they only deduplicate within a single translation unit and say nothing about the other files.