Chapter 1 gave you the building blocks — types, functions, and the machine mapping. Chapter 2 gave you the first abstraction mechanism — user-defined types with encapsulation. Now you need a way to put those types (and the functions that use them) into separate, independently compilable pieces. That is the subject of this chapter: modules, header files, namespaces, and the full story on how arguments are passed and values returned.
Why separate compilation matters now: a real program consists of many source files. Compiling them independently — and linking them afterward — is the foundation of every C++ project. Without it, every change would require recompiling everything. With it, you get faster iteration, cleaner dependency graphs, and the ability to ship libraries whose implementation details are hidden from users.
A C++ program is made of separately developed parts: functions (§1.2.1), user-defined types (Chapter 2), class hierarchies (§5.5), and templates (Chapter 7). The key to managing all these parts is to clearly define the interactions among them. The first and most important step is distinguishing between the interface to a part and its implementation.
At the language level, C++ represents interfaces with
declarations. A declaration says what a function or type looks like — its
name, parameter types, return type — without saying how it works. Here is a declaration for
sqrt and a class Vector:
double sqrt(double);
class Vector { // what is needed to use a Vector
public:
Vector(int s);
double& operator[](int i);
int size();
private:
double* elem; // elem points to an array of sz doubles
int sz;
};
// the square root function takes a double and returns a double
Mind your own business — that is the principle behind modularity. The body of a function (its
definition) can be "elsewhere," and the representation of a class (its private members) can
also be elsewhere. For Vector, we need to define all three member functions
eventually — but the user of Vector never needs to see the definition to use it.
Vector::Vector(int s) // definition of the constructor
: elem{new double[s]}, sz{s} // initialize members
{}
double& Vector::operator[](int i) // definition of subscripting
{ return elem[i]; }
int Vector::size() // definition of size()
{ return sz; }
An entity can have many declarations but only one definition. A
translation unit is a .cpp file that is compiled by itself, including all the
header files it #include. A program can consist of thousands of translation
units.
C++ supports separate compilation in two ways: header files (§3.2.1) and modules (§3.2.2). Either organizes a program into semi-independent code fragments that can be compiled separately, minimized compilation times, and enforce clean interfaces.
.h files; implementations
live in .cpp files. #include pastes the header text into every
translation unit that needs it.
exported declarations are visible. The effects on
compile times and code hygiene are dramatic (see §3.2.2).
Traditionally we place declarations for a piece of code in a file whose name indicates its
purpose, with a .h extension. For Vector: Vector.h.
Users #include that file to get the interface.
Vector.h — the interface:
// Vector.h: class Vector {
public:
Vector(int s);
double& operator[](int i);
int size();
private:
double* elem; // elem points to an array of sz doubles
int sz;
};
user.cpp — uses Vector through the header:
#include "Vector.h" // get Vector's interface
#include <cmath> // get the standard-library math interface (sqrt)
double sqrt_sum(const Vector& v) {
double sum = 0;
for (int i = 0; i != v.size(); ++i)
sum += std::sqrt(v[i]); // sum of square roots
return sum;
}
Vector.cpp — the implementation, also includes the header it implements:
#include "Vector.h" // get Vector's interface
Vector::Vector(int s)
: elem{new double[s]}, sz{s} // initialize members
{}
double& Vector::operator[](int i) { return elem[i]; }
int Vector::size() { return sz; }
The code in user.cpp and Vector.cpp shares the Vector interface from Vector.h, but the two files are otherwise independent and can be separately compiled:
// compile separately:
// g++ -c user.cpp -o user.o
// g++ -c Vector.cpp -o Vector.o
// g++ user.o Vector.o -o myprogram -lm
The header-file technique for organizing code goes back to the earliest days of C and C++, and it has significant disadvantages:
#include a header in 101 translation
units, the compiler processes the header's text 101 times.
These problems have been a major source of cost and bugs since C's earliest days. Header files remain viable because updating large programs away from them is costly and time-consuming. But they are not the best tool for the job.
In C++20, we finally have a language-supported way of directly expressing modularity. Consider the Vector and sqrt_sum example expressed as modules:
export module Vector; // defining the module called "Vector"
export class Vector {
public:
Vector(int s);
double& operator[](int i);
int size();
private:
double* elem;
int sz;
};
Vector::Vector(int s) : elem{new double[s]}, sz{s} {}
double& Vector::operator[](int i) { return elem[i]; }
int Vector::size() { return sz; }
export bool operator==(const Vector& v1, const Vector& v2) {
if (v1.size() != v2.size()) return false;
for (int i = 0; i != v1.size(); ++i)
if (v1[i] != v2[i]) return false;
return true;
}
This defines a module called Vector that exports the class, all its member functions, and the non-member operator==.
import// file user.cpp:
import Vector; // get Vector's interface (not pasted, compiled once)
#include <cmath> // get the standard-library math interface (sqrt)
double sqrt_sum(Vector& v) {
double sum = 0;
for (int i = 0; i != v.size(); ++i)
sum += std::sqrt(v[i]); // sum of square roots
return sum;
}
I could have imported the standard-library mathematical functions too, but I used
the old-fashioned #include just to show that old and new styles can be mixed.
Such mixing is essential for gradually upgrading older code from #include to
import.
Why modules are fundamentally better: a module is compiled once only. Two
modules can be imported in either order without changing their meaning. If you import
something into a module, users of your module do not implicitly gain access —
import is not transitive. The effects on maintainability and compile-time
performance can be spectacular. I have measured the "Hello, World!" program using
import std;
compile 10 times faster than the version using
#include <iostream>, despite std containing the whole standard library.
From here on, I assume import std for all examples. Appendix A explains how to
get a module std if a standard-library implementation doesn't yet supply it.
When defining a module, we don't have to separate declarations and definitions into different
files — we can if it improves organization, but we don't have to. The compiler separates a
module's interface (specified by export
specifiers) from its implementation details. The exported interface is generated by the
compiler and never explicitly named by the user. Using modules, we don't have to complicate
our code to hide implementation details; a module only grants access to exported declarations.
export module vector_printer;
import std;
export template<typename T>
void print(std::vector<T>& v) { // the only function seen by users
cout << "{\n";
for (const T& val : v)
std::cout << " " << val << '\n';
cout << '}';
}
By importing this trivial module, we don't suddenly gain access to all of the standard library. The template parameter is how we parameterize a function with a type (§7.2).
In addition to functions (§1.3), classes (§2.3), and enumerations (§2.4), C++ offers namespaces — a mechanism for expressing that some declarations belong together and that their names shouldn't clash with other names. For example:
namespace My_code {
class complex { /* ... */ };
complex sqrt(complex);
int main();
}
int My_code::main() {
complex z{1, 2};
auto z2 = sqrt(z);
std::cout << '{' << z2.real() << ',' << z2.imag() << "}\n";
}
int main() { return My_code::main(); }
The simplest way to access a name in another namespace is to qualify it with the namespace
name (std::cout, My_code::main). If repeatedly qualifying a name
becomes tedious or distracting, a using-declaration brings a single name into
scope:
void my_code(vector<int>& x, vector<int>& y) {
using std::swap; // make std::swap available locally
swap(x, y); // calls std::swap (or another overload)
}
A using-declaration makes a name from a namespace usable as if it were declared in the scope
in which it appears. After using std::swap;, it is exactly as if
swap had been declared in my_code(), with argument-dependent lookup
still choosing the best overload.
To gain access to all names in a namespace, use a using-directive:
using namespace std;. This makes unqualified names from std accessible from the
scope in which the directive appears:
export module vector_printer;
import std;
using namespace std; // implementation detail, local to this module
export template<typename T>
void print(vector<T>& v) { // can use unqualified "vector", "cout"
cout << "{\n";
for (const T& val : v)
cout << " " << val << '\n';
cout << '}';
}
Importantly, this use of the namespace directive does not affect users of our module — it is an implementation detail, local to the module. By using a using-directive we lose the ability to selectively use names from that namespace, so this facility should be used carefully, usually for a library that is pervasive in an application (e.g., std) or during a transition for an application that didn't use namespaces.
Namespaces are primarily used to organize larger program components — libraries. They simplify the composition of a program out of separately developed parts.
The primary and recommended way of passing information between parts of a program is through a function call. Information needed to perform a task is passed as arguments; the results are passed back as return values. The default behavior for both argument passing and value return is to make a copy (§1.9), but many copies can be implicitly optimized to moves.
By default we copy (pass-by-value). If we want to refer to an object in the caller's environment, we use a reference (pass-by-reference). If we want to refer without modifying, we use a const-reference — by far the most common case in ordinary good code: fast and not error-prone.
Copy vs. reference vs. const-reference:
void test(vector<int> v, vector<int>& rv)
// v is passed by value (copied); rv is passed by reference
{
v[1] = 99; // modifies only the local copy
rv[2] = 66; // modifies the caller's vector
}
int main() {
vector<int> fib = {1, 2, 3, 5, 8, 13, 21};
test(fib, fib);
cout << fib[1] << ' ' << fib[2] << '\n'; // prints 2 66
}
When performance matters, we pass small values by-value and larger ones by-reference-to-const. "Small" means something cheap to copy — about the size of two or three pointers or less. If it might be significant to your performance, measure.
Default arguments: a value that is considered preferred or the most common case can be specified as a default:
void print(int value, int base = 10); // print value in base "base"
print(x, 16); // hexadecimal
print(x, 60); // sexagesimal (Sumerian)
print(x); // use the default: decimal
A default argument is a notationally simpler alternative to overloading — one definition handles both the default and the explicit case. Using default arguments means there is only one definition of the function — usually good for comprehension and code size. When different code is needed for different types, overloading is the right choice.
Argument passing summary for built-in types:
| Scenario | Pass as | Why |
|---|---|---|
| Small built-in (int, double, pointer) | by value | Cheap to copy; no aliasing risk |
| Large built-in (struct wider than 2-3 pointers) | by const-reference | Avoids copy cost; no mutation |
| User-defined type you don't want to copy | by const-reference | Standard idiom for read-only access |
| User-defined type you want to modify | by non-const reference | Call-by-reference allows mutation |
| Optional modification (move semantics) | by value (for movable types) | Compiler may optimize copy to move |
Once a result is computed, it needs to get back to the caller. The default for value return is to copy; for small objects that's ideal. We return by reference only when we want to grant the caller access to something local to the function.
Good — returning a reference to a member:
class Vector {
public:
double& operator[](int i) { return elem[i]; } // return reference to element
private:
double* elem;
};
The i-th element of a Vector exists independently of any call to operator[], so we can return a reference to it. The caller reads or writes the element through the reference.
Bad — returning a reference to a local variable:
int& bad() {
int x; // local variable, lives on the stack
return x; // bad: x is destroyed when bad() returns; dangling reference
}
Fortunately, all major C++ compilers will catch the obvious error in
bad() (with a warning at minimum, a hard error at best for returning a reference
to a local non-static variable).
Returning a reference or a value of a small type is efficient. For large objects we face the question: how do we pass a large result out of a function without copying it? The answer: rely on move semantics and copy elision. A Matrix is expensive to copy but cheap to move:
Matrix operator+(const Matrix& x, const Matrix& y) {
Matrix res; // default-construct result
// ... compute res[i,j] = x[i,j] + y[i,j] ...
return res; // move or copy-elide: no deep copy
}
Matrix m1, m2;
Matrix m3 = m1 + m2; // no copy — move or elision
We should not regress to manual memory management: returning a pointer to a heap-allocated object is the "20th century style" — a "complicated and error-prone" pattern that is a major source of memory leaks and dangling pointers in older code. Don't write such code.
auto)The return type of a function can be deduced from its return value. The compiler looks at what the function returns and uses that as the declared return type:
auto mul(int i, double d) { return i * d; }
// "auto" here means "deduce the return type" → double
This can be convenient — especially for generic functions (§7.3.1) and lambdas (§7.3.3) — but should be used carefully because a deduced type does not offer a stable interface: a change to the implementation of the function can change its type silently, and callers won't know until compile time breaks.
Why does the return type come before the function name and arguments? The reason is mostly
historical — tradition from C, Simula, and Fortran. However, sometimes we need to look at the
arguments to determine the type of the result. Return type deduction is one example; other
examples include namespaces (§3.3), lambdas (§7.3.3), and concepts (§8.2). C++ allows adding
the return type after the argument list — called a suffix return type — using
the -> syntax:
auto mul(int i, double d) -> double {
return i * d; // the return type is explicitly "double"
}
After ->, auto means "the return type will be mentioned later or be
deduced (§3.4.3)." Use the suffix form when the type depends on the arguments (e.g., an
operator that returns the common type of its operands) or when readability demands it. For
simple cases where the type is independent of the arguments, the prefix form is idiomatic.
Stroustrup closes the chapter with a subset of the C++ Core Guidelines. All 9 items, with the section where each is introduced:
| # | Guideline | § |
|---|---|---|
| 1 | Think of a program as a set of modules with well-defined dependencies | 3.2 |
| 2 | Use header files for declarations and separate compilation to organize code | 3.2.1 |
| 3 | Prefer modules over header files in new code | 3.2.2 |
| 4 | Use namespaces to prevent name clashes | 3.3 |
| 5 | Avoid using-directives in header files | 3.3 |
| 6 | Pass arguments by const-reference when you need read-only access to large objects | 3.4.1 |
| 7 | Return by value; rely on move semantics and copy elision for large objects | 3.4.2 |
| 8 | Never return a reference or pointer to a local variable | 3.4.2 |
| 9 | Use auto for simple return types; consider suffix return type when the type depends on argument types | 3.4.3, 3.4.4 |
How many times is a header file's text processed by the compiler if it is included in 100 translation units?
In a C++20 module, what makes a declaration visible to importers?
What is the key difference between using-std::swap; and using namespace std;
Why is return x; inside int& bad() { int x; ... } a bug?
What does auto in the suffix position (auto ... -> Type) mean?
You have a function that reads a large vector but never modifies it. How should you pass it?
program ⇒ many separately developed parts ; key ⇒ clearly defined interactions
declaration ⇒ interface ; all that's needed to use a function/type
definition ⇒ implementation ; all that's needed to run
a) header ⇒ textually #included ; declarations shared
disadvantages ⇒ compile-time cost, recompilation, ordering, no structure enforcement
b) module ⇒ compiled once ; export module + import
non-transitive import ⇒ imported names not re-exported ; export marks
visible names
namespace ⇒ prevents name clashes ; expresses logical structure
using-declaration ⇒ brings one name (eg
using std::cout)
using-directive ⇒ brings every name (eg
using namespace std)
ADL ⇒ argument-dependent lookup ; finds function via argument's namespace
a) by value ⇒ copy ; small types (eg int, double)
b) by reference ⇒ no copy ; allows modification
c) by const reference ⇒ no copy + read-only ; default for large objects
default arguments ⇒ omitted trailing args filled (eg
f(x, y=2))
return by value ⇒ move semantics + copy elision → cheap
member reference return ⇒ ok (eg operator[] returns
double&)
local reference return ⇒ dangling ; never ×
auto ⇒ return type deduced ; suffix -> Type ⇒ when
type depends on args
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 3: "Separate Compilation, Modules, and Namespaces." Addison-Wesley.
Reference:
Chapter 1–3 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
namespaces,
C++20 modules, and
function arguments and return.
Questions? Ask your agent — your teacher — about anything unclear: a quiz answer, the distinction between using-declaration and using-directive, or how separate compilation maps to a real multi-file project structure. Follow-ups are expected, not optional.