본문으로 건너뛰기 [2026] C++ Compilation Pipeline — Preprocessing, Compilation, Assembly, Linking (Why "undefined reference" Happens)

[2026] C++ Compilation Pipeline — Preprocessing, Compilation, Assembly, Linking (Why "undefined reference" Happens)

[2026] C++ Compilation Pipeline — Preprocessing, Compilation, Assembly, Linking (Why "undefined reference" Happens)

이 글의 핵심

A practical walkthrough of the four-stage C++ build: preprocessing (#include, macros), compilation (AST), assembly (machine code), and linking (object files and libraries)—plus static vs dynamic linking and how to fix undefined reference errors.

[C++ Practical Guide #5] Understanding the C++ compilation pipeline

After reading this article, you will understand preprocessing, compilation, assembly, and linking; the difference between static and dynamic libraries; and how to approach undefined reference errors.

The single command g++ main.cpp -o main produces an executable, but internally it runs several stages. An analogy: like cooking—prep → chop → cook → plate—it is not one atomic step; each stage produces intermediate artifacts. Understanding this helps you fix compiler errors faster and optimize build times.

This article explains how source code becomes an executable by splitting the process into four stages: preprocessing, compilation, assembly, and linking.

Table of contents

  1. Compilation pipeline overview
  2. Problem scenarios: why learn this?
  3. Preprocessing
  4. Compilation
  5. Assembly
  6. Linking
  7. End-to-end example (multi-file project)
  8. Common errors and fixes
  9. Best practices
  10. Compilation optimization tips
  11. Production build patterns

1. Compilation pipeline overview

When you run g++ main.cpp -o main, the toolchain conceptually performs these four stages:

flowchart LR
  A[.cpp source] --> B[Preprocessing]
  B --> C[.i]
  C --> D[Compilation]
  D --> E[.s assembly]
  E --> F[Assembly]
  F --> G[.o object]
  G --> H[Linker]
  H --> I[Executable]
Source code (.cpp)
    ↓ 1. Preprocessor
Preprocessed code (.i)
    ↓ 2. Compiler
Assembly code (.s)
    ↓ 3. Assembler
Object file (.o)
    ↓ 4. Linker
Executable

Each stage has a different role and can emit intermediate files. In plain terms: preprocessing expands #include and macros; compilation parses syntax and lowers to an intermediate representation; assembly turns that into CPU instructions; linking combines multiple object files (.o)—the per-translation-unit output after compile+assemble, not yet connected to other objects (analogy: a part before final assembly). Understanding this lets you tell which stage failed when something breaks.

For example, “cannot find header” usually points to preprocessing; “syntax error” to compilation; “undefined symbol / reference” often to linking. Mapping error messages to stages narrows the root cause quickly.

When you see a link error: undefined reference to 'function_name' usually means a declaration exists, but the definition was not linked into this build. If the function lives in another .cpp, ensure that file is part of add_executable or your compile command; if it comes from a library, link with -lname or CMake target_link_libraries. See Part 49-2: CMake link errors for LNK2019-style patterns.

Practical tip: g++ -E main.cpp runs preprocessing only so you can see how #include expands. For link errors, read the undefined reference to ... lines—they mean declared but not defined / not linked. If the definition is in another .cpp, add it to the build or link the right library.


2. Problem scenarios: why learn the pipeline?

Scenario 1: undefined reference to 'foo()'

Situation: main.cpp calls foo(), compilation succeeds, linking fails.

Cause: foo is declared in a header, but its definition is not part of the link. Preprocessing and compilation only need a declaration; linking must find the actual machine code.

Fix: Add the .cpp that defines foo to the build, or link the library that contains foo with -l.

Scenario 2: Headers explode to thousands of lines and builds slow down

Situation: Even #include <iostream> can expand to tens of thousands of lines; more headers multiply compile time.

Cause: Preprocessing literally inserts everything from #included headers. <iostream> pulls in many other standard headers.

Fix: Include only what you need; C++20 modules can shrink preprocessing work. Precompiled headers (PCH) also help.

Scenario 3: redefinition of 'class X'

Situation: A header included from multiple .cpp files triggers redefinition errors.

Cause: The header contains a definition (not just declarations). Each .cpp emits a copy; the linker then sees duplicate symbols.

Fix: Headers hold declarations; put definitions in .cpp files. Templates and inline functions are common exceptions. Use #pragma once or include guards to prevent multiple inclusion of the same header.

Scenario 4: The linker cannot find a library

Situation: You pass -lboost_system but get cannot find -lboost_system.

Cause: The linker search path does not contain libboost_system.a / .so, or the name is wrong.

Fix: Add directories with -L (e.g. -L/usr/local/lib). The -lfoo flag corresponds to libfoo.a or libfoo.so (strip the lib prefix and extension).

Scenario 5: Your edit does not show up in the binary

Situation: You changed a header, but make still behaves as if nothing changed.

Cause: The Makefile (or script) does not encode “recompile these .cpp files when this header changes,” or a stale cache is reused.

Fix: Let CMake or similar track header dependencies; make clean and rebuild; clear ccache if needed.

Scenario 6: ABI-incompatible libraries

Situation: Linking a library built with another compiler/standard crashes at runtime.

Fix: Build everything with the same toolchain and C++ standard. Wrap C APIs with extern "C" when required.

Scenario 7: Circular includes break the build

Situation: A.h includes B.h and B.h includes A.h, yielding incomplete type errors.

Cause: Types are used before they are fully defined.

Fix: Forward-declare in headers (class B;), and #include only in .cpp files where needed.

Situation: -lutils -lmylib works but -lmylib -lutils fails.

Cause: With static linking, the linker may scan archives left-to-right once; if mylib depends on utils, order matters.

Fix: Put dependents before dependencies, or use --start-group / --end-group for cycles.


3. Preprocessing

What the preprocessor does

In short, preprocessing runs before the compiler parses C++ grammar: it pastes headers, applies macros, and selects conditional code.

  • #include pastes header contents
  • #define expands macros
  • #ifdef / #ifndef select code
flowchart TD
  A[Source] --> B{#include}
  B --> C[Insert headers]
  C --> D{#define}
  D --> E[Macro substitution]
  E --> F{#ifdef}
  F --> G[Include/exclude conditional code]
  G --> H[Preprocessed .i file]

Example

The following main.cpp is a minimal demo. #include <iostream> is expanded to the full header text before compilation; #define PI 3.14159 replaces every PI with 3.14159, so after preprocessing the name PI disappears.

main.cpp (you can build with g++ main.cpp -o main && ./main):

// After paste: g++ main.cpp -o main && ./main
#include <iostream>
#define PI 3.14159

int main() {
    std::cout << "PI = " << PI << std::endl;
    return 0;
}

Output:

PI = 3.14159

Preprocess only with -E. That skips compile, assemble, and link, and writes the expanded translation unit to main.i.

g++ -E main.cpp -o main.i

Opening main.i, you will see thousands of lines from <iostream> where the include was, and PI replaced by 3.14159. Preprocessing does not perform full C++ semantic analysis—it applies directives and text substitution.

Sketch of main.i:

// Thousands of lines from iostream...
int main() {
    std::cout << "PI = " << 3.14159 << std::endl;
    return 0;
}

Include guard example

Use an include guard so headers are not processed multiple times in one translation unit.

// config.h — prevent duplicate inclusion with an include guard
#ifndef CONFIG_H
#define CONFIG_H

#define APP_VERSION "1.0.0"
#define MAX_BUFFER_SIZE 4096

#endif  // CONFIG_H

Note: macro names must be unique across your project; short names like CONFIG_H can collide with third-party code.

With compilers that support it, #pragma once is simpler:

// config.h
#pragma once

#define APP_VERSION "1.0.0"

4. Compilation

Compiler pipeline

Typical compiler stages:

Preprocessed code (.i)

1. Lexical analysis:

   Input: int x = 42;

   Token stream:
   [INT_KEYWORD] [IDENTIFIER:x] [EQUAL] [NUMBER:42] [SEMICOLON]

   Lexical errors:
   int 123abc = 42;  // error: identifier cannot start with a digit

2. Syntax analysis:

   Tokens → parse tree → AST

   Example:
   int x = 42;

   AST:
   VarDecl (x)
   ├─ Type: int
   └─ InitExpr
      └─ IntegerLiteral: 42

   More complex:
   int add(int a, int b) {
       return a + b;
   }

   AST:
   FunctionDecl (add)
   ├─ ReturnType: int
   ├─ Parameters
   │  ├─ ParmVarDecl (a, int)
   │  └─ ParmVarDecl (b, int)
   └─ Body: CompoundStmt
      └─ ReturnStmt
         └─ BinaryOperator (+)
            ├─ DeclRefExpr (a)
            └─ DeclRefExpr (b)

   Syntax errors:
   int x = ;  // error: missing expression
   if (x) }   // error: missing '{'

3. Semantic analysis:

   Type checking:
   int x = "hello";  // error: cannot convert const char* to int

   Scope:
   int main() {
       int x = y;  // error: y not declared
   }

   Overload resolution:
   void foo(int);
   void foo(double);
   foo(42);  → selects foo(int)

   Template instantiation:
   template<typename T>
   T max(T a, T b) { return a > b ? a : b; }

   max(3, 5);       → max<int>
   max(3.14, 2.71); → max<double>

4. IR generation:

   Platform-independent intermediate representation

   LLVM IR example:
   define i32 @add(i32 %a, i32 %b) {
     %1 = add nsw i32 %a, %b
     ret i32 %1
   }

   GCC GIMPLE sketch:
   int add (int a, int b) {
     int D.1234;
     D.1234 = a + b;
     return D.1234;
   }

5. Optimization:

   Constant folding:
   int x = 2 + 3;
   → int x = 5;

   Dead code elimination:
   if (false) {
       printf("never");  // removed
   }

   Inlining:
   inline int square(int x) { return x * x; }
   int y = square(5);
   → int y = 25;

   Loop optimizations:
   for (int i = 0; i < 1000; i++) {
       arr[i] = 0;
   }
   → memset(arr, 0, 1000 * sizeof(int));

   Vectorization (SIMD):
   for (int i = 0; i < 8; i++) {
       c[i] = a[i] + b[i];
   }
   → vaddps xmm0, xmm1, xmm2  // example

6. Code generation:

   IR → target assembly

   int add(int a, int b) {
       return a + b;
   }

   x86-64:
   add:
       lea eax, [rdi+rsi]
       ret

   AArch64:
   add:
       add w0, w0, w1
       ret

Optimization levels (-O0, -O1, -O2, -O3):

-O0 (no optimization):
- Easier debugging
- Faster compile
- Slower runtime

-O2 (typical release):
- Constant folding, inlining, loop opts
- Moderate compile time
- Fast runtime

-O3 (aggressive):
- More vectorization and unrolling
- Slower compile
- Often fastest runtime

Example:

int sum = 0;
for (int i = 0; i < 1000; i++) {
    sum += arr[i];
}

-O0 assembly (sketch):
  mov eax, 0
  mov ecx, 0
.L2:
  cmp ecx, 1000
  jge .L3
  add eax, [arr+rcx*4]
  inc ecx
  jmp .L2

-O3 assembly (vectorized sketch):
  xorps xmm0, xmm0
  mov ecx, 0
.L2:
  movups xmm1, [arr+rcx]
  addps xmm0, xmm1
  add ecx, 16
  cmp ecx, 4000
  jl .L2
  • Syntax and semantic checks
  • Optimization
  • Assembly generation

Example

Turn preprocessed main.i into assembly (.s) with -S. This is where C++ grammar is fully analyzed, optimizations run, and CPU-level instructions are emitted.

g++ -S main.i -o main.s

main.s shows main lowered to instructions like push, mov, call. push rbp / mov rbp, rsp are the function prologue; long call targets are runtime support for std::cout. This file shows how high-level C++ maps to machine-level operations.

main.s (excerpt):

main:
    push    rbp
    mov     rbp, rsp
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    ...

C++ name mangling

Overloaded functions get mangled symbol names in assembly and object files. Names like _ZSt4cout represent std::cout. Use nm main.o to inspect symbols.

nm main.o | grep main
# 0000000000000000 T main

5. Assembly

What the assembler does

  • Translates assembly to machine code
  • Writes an object file

Example

-c means “compile to an object file, do not link.” Given main.s, the assembler emits main.o containing relocatable machine code. The .o is still a fragment—it is not yet merged with other objects or libraries.

g++ -c main.s -o main.o

file main.o typically reports something like ELF 64-bit LSB relocatable, x86-64—a relocatable object for the linker to combine later.

file main.o
main.o: ELF 64-bit LSB relocatable, x86-64

6. Linking

What the linker does

  • Merges object files
  • Attaches libraries
  • Resolves symbols
flowchart LR
  subgraph inputs[Inputs]
    A[main.o]
    B[utils.o]
    C[libc.a]
  end
  subgraph linker[Linker]
    D[Symbol resolution]
    E[Address assignment]
    F[Relocation]
  end
  subgraph output[Output]
    G[Executable]
  end
  A --> D
  B --> D
  C --> D
  D --> E --> F --> G

Example

Split sources compile to separate .o files; the linker produces one executable. utils.h declares add; #pragma once avoids duplicate inclusion of the same header. utils.cpp defines add; main.cpp includes utils.h and calls add. Each .cpp compiles without seeing the other’s full definitions, but the linker joins main.o and utils.o and patches the call sites.

utils.h:

#pragma once
int add(int a, int b);

utils.cpp:

#include "utils.h"

int add(int a, int b) {
    return a + b;
}

main.cpp:

#include <iostream>
#include "utils.h"

int main() {
    std::cout << "2 + 3 = " << add(2, 3) << std::endl;
    return 0;
}

Commands 1–2 compile each .cpp to main.o / utils.o; 3 links them into myapp; 4 runs the program.

# Compile separately
g++ -c main.cpp -o main.o
g++ -c utils.cpp -o utils.o

# Link
g++ main.o utils.o -o myapp

# Run
./myapp
2 + 3 = 5

Static vs dynamic libraries

Static libraries (.a / .lib)

  • Code copied into the executable
  • Larger binaries
  • No runtime library dependency for that code

A static library archives .o files (often with ar). ar rcs creates/updates the archive. -L. adds the current directory to the library search path; -lutils links libutils.a. Used symbols from libutils.a are copied into myapp, so you can run myapp without shipping libutils.a separately.

# Build a static library
ar rcs libutils.a utils.o

# Link
g++ main.o -L. -lutils -o myapp

Dynamic libraries (.so / .dll)

  • Loaded at runtime
  • Smaller executables
  • The shared library must be present (or found) at runtime

Build shared objects with -shared -fPIC. -shared builds a shared library; -fPIC emits position-independent code so multiple processes can share one mapping. Linking main.o with libutils.so does not copy add into the executable; the dynamic loader resolves it when the program starts. You may need LD_LIBRARY_PATH=. (Linux) or equivalent so the loader finds libutils.so.

# Shared library
g++ -shared -fPIC utils.cpp -o libutils.so

# Link
g++ main.o -L. -lutils -o myapp

# Run (example: library in current directory on Linux)
LD_LIBRARY_PATH=. ./myapp

7. End-to-end example (multi-file project)

Step-by-step: hello.cpp → executable

// hello.cpp
#include <cstdio>
#define GREET "Hello"
#define SQUARE(x) ((x) * (x))
int main() { printf("%s, %d\n", GREET, SQUARE(3)); return 0; }
StepCommandResult
1. Preprocessg++ -E hello.cpp -o hello.iGREET"Hello", SQUARE(3)((3)*(3)), stdio inserted
2. Compileg++ -S hello.i -o hello.sAssembly (push, mov, call, …)
3. Assembleg++ -c hello.s -o hello.oELF object; nm shows main (T), printf (U)
4. Linkg++ hello.o -o helloExecutable linked against libc
flowchart LR
  A[hello.cpp] -->|g++ -E| B[hello.i]
  B -->|g++ -S| C[hello.s]
  C -->|g++ -c| D[hello.o]
  D -->|g++| E[hello]

Example 1: three-file calculator (calc.h, calc.cpp, main.cpp)

// calc.h
#pragma once
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);

// calc.cpp — implementations
// main.cpp — calls add, subtract, multiply
g++ -std=c++17 main.cpp calc.cpp -o calculator
./calculator  # e.g. 10+5=15, 10-5=5, 10*5=50

Example 2: static library

g++ -c calc.cpp -o calc.o
ar rcs libcalc.a calc.o
g++ -c main.cpp -o main.o
g++ main.o -L. -lcalc -o calculator

Example 3: one-shot build

g++ main.cpp utils.cpp -o myapp runs preprocess → compile → assemble → link in one go.

Example 4: build systems

Production code typically uses Make, CMake, Ninja, etc. They orchestrate the same four stages with dependency tracking and parallelism.

Makefile: dependency-driven incremental builds

# Makefile — calculator project
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
TARGET = calculator
OBJS = main.o calc.o

$(TARGET): $(OBJS)
	$(CXX) $(OBJS) -o $(TARGET)

%.o: %.cpp calc.h
	$(CXX) $(CXXFLAGS) -c $< -o $@

clean:
	rm -f $(OBJS) $(TARGET)
.PHONY: clean

Run make or make -j4. Editing calc.h rebuilds main.o and calc.o, then relinks.

CMake + Ninja

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Calculator LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(calculator main.cpp calc.cpp)
target_include_directories(calculator PRIVATE ${CMAKE_SOURCE_DIR})
mkdir build && cd build
cmake -G Ninja ..
ninja -j$(nproc)

CMake can track header dependencies automatically. Summary: Make, CMake, and Ninja all drive the same compiler stages; differences are dependency graphs, parallelism, and portability.


8. Common errors and fixes

Error 1: undefined reference to 'function_name'

Example message:

/tmp/ccXYZ123.o: In function `main':
main.cpp:(.text+0x15): undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status

Cause: add is declared but its definition is not linked.

Fix:

# Bad: only main.cpp
g++ main.cpp -o myapp

# Good: compile the .cpp that defines add too
g++ main.cpp utils.cpp -o myapp

Error 2: multiple definition of 'function_name'

Example message:

utils.o: In function `add(int, int)':
utils.cpp:4: multiple definition of `add(int, int)'
main.o:main.cpp:4: first defined here

Cause: A definition lived in a header included from multiple .cpp files, so each object file contained a copy.

Fix: declarations in headers, single definition in one .cpp.

// Bad: definition in header
// utils.h
#pragma once
int add(int a, int b) { return a + b; }

// Good: declaration only in header
// utils.h
#pragma once
int add(int a, int b);

Error 3: fatal error: myheader.h: No such file or directory

Example message:

main.cpp:2:10: fatal error: myheader.h: No such file or directory
    2 | #include "myheader.h"

Cause: The preprocessor cannot find the header (wrong path / missing -I).

Fix:

g++ -I./include -I/usr/local/include main.cpp -o main

Error 4: cannot find -llibrary

Example message:

/usr/bin/ld: cannot find -lboost_system
collect2: error: ld returned 1 exit status

Cause: libboost_system.a or .so is not on the linker search path.

Fix:

g++ main.o -L/usr/local/lib -lboost_system -o myapp

# Check installed libs (Linux)
ldconfig -p | grep boost

Error 5: undefined reference to 'vtable for ClassName'

Cause: Virtual functions declared but not defined, or the implementing .cpp is missing from the build.

Fix: Add the .cpp that defines the virtuals.

Error 6: Mixing C and C++ — undefined reference

Cause: C symbols are named differently from mangled C++ symbols.

Fix: Wrap C headers with extern "C".

// mylib.h
#ifdef __cplusplus
extern "C" {
#endif

void c_function(int x);

#ifdef __cplusplus
}
#endif

Error 7: symbol lookup error (dynamic library)

Example message:

./myapp: symbol lookup error: ./myapp: undefined symbol: _ZN5Utils3addEii

Cause: Wrong .so at runtime, missing library path, or ABI mismatch.

Fix:

LD_LIBRARY_PATH=/path/to/libs ./myapp

# Or embed rpath
g++ main.o -L. -lutils -Wl,-rpath,'$ORIGIN' -o myapp

Error 8: redefinition of 'struct/class'

Cause: Missing include guards or duplicate class definitions across headers.

Fix: #pragma once or traditional guards; one canonical definition per type.

Error 9: undefined reference to typeinfo / vtable

Cause: Virtual class .cpp missing from the link, or RTTI disabled with -fno-rtti while still needed.

Fix: Link the implementation; align RTTI flags across TUs.

Error 10: relocation truncated to fit

Cause: Building a shared library without -fPIC.

Fix: Add -fPIC for shared objects.

Error 11: Template instantiation errors

Cause: Template definitions only in .cpp without explicit instantiation for used specializations.

Fix: Put templates in headers, or add explicit instantiations.

Cause: -flto with incompatible objects/libraries.

Fix: Drop LTO (-fno-lto) or use LTO-compatible builds throughout.

Error 13: C++ standard mismatch

Cause: Mixing objects built with different -std= settings.

Fix: One standard for the whole project (e.g. set(CMAKE_CXX_STANDARD 17)).

Error 14: Windows min/max macros

Cause: Windows headers define min/max macros that break std::min / std::max.

Fix: #define NOMINMAX before #include <windows.h>.

Error cheat sheet

SymptomStageTypical causeDirection
fatal error: ... No such filePreprocessMissing include pathAdd -I
syntax error, expected ';'CompileInvalid C++Fix source
undefined referenceLinkMissing definition / libraryAdd .cpp or -l
multiple definitionLinkDefinitions in headers / duplicate objectsSplit decl/def
cannot find -lxxxLinkWrong -L or missing installFix paths
vtable / typeinfo undefinedLinkMissing virtual impl .cppAdd TU
relocation truncatedLinkMissing -fPICUse -fPIC for .so
symbol lookup errorRuntime.so not foundLD_LIBRARY_PATH / rpath
LTO plugin failedLinkIncompatible LTO mixDisable LTO or rebuild all
C++ standard mismatchRuntimeABI skewSame -std everywhere
min/max issuesPreprocess/compileWindows headersNOMINMAX / parens

9. Best practices

BP1: Header design

  • Separate declarations and definitions: headers declare; .cpp defines (except inline, templates, constexpr in headers as appropriate).
  • Include guards: #pragma once or #ifndef/#define/#endif.
  • Minimal includes: prefer forward declarations when you only need pointers/references.
// Good: utils.h
#pragma once
int add(int a, int b);

// Good: utils.cpp
#include "utils.h"
int add(int a, int b) { return a + b; }

BP2: Use a real build system

  • CMake (or similar) for portability and dependency tracking.
  • Declare link edges explicitly with target_link_libraries.

BP3: Compiler warnings

-Wall -Wextra catches many mistakes early; CI can use -Werror to enforce.

Put libraries after their dependents. If mylib needs utils, order like -lutils -lmylib. For cycles, -Wl,--start-group--end-group.

BP5: Debug vs release

  • Debug: -O0 -g for symbols and predictable stepping.
  • Release: -O2 / -O3; optionally strip to shrink binaries.

BP6: Stage-by-stage debugging

g++ -E main.cpp -o main.i   # preprocess only
g++ -S main.cpp -o main.s   # stop at assembly
g++ -c main.cpp -o main.o && nm main.o  # inspect symbols
g++ main.o utils.o -o myapp -v  # verbose link

BP7: pkg-config

g++ main.cpp -o main $(pkg-config --cflags --libs openssl)

10. Compilation optimization tips

Tip 1: Parallel builds

make -j$(nproc)

cmake --build . -j$(nproc)

Tip 2: ccache

export CC="ccache gcc"
export CXX="ccache g++"

Tip 3: Precompiled headers (PCH)

// pch.h
#pragma once
#include <iostream>
#include <vector>
#include <string>
g++ -std=c++17 -x c++-header pch.h -o pch.h.gch
g++ -std=c++17 -include pch.h main.cpp -o main

Tip 4: Remove unused headers

// Avoid including large headers you do not need

// Prefer forward declarations when possible
class MyClass;
void useMyClass(MyClass& obj);

Tip 5: Incremental builds

CMake/Make rebuild only what changed—if header dependencies are correct, touching a header rebuilds the right .cpp files.

Tip 6: Optimization levels

g++ -O0 -g main.cpp -o main   # debug
g++ -O2 main.cpp -o main      # release

11. Production build patterns

Pattern 1: Cross-platform CMake project

cmake_minimum_required(VERSION 3.16)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

add_executable(myapp
    main.cpp
    calc.cpp
)

target_include_directories(myapp PRIVATE ${CMAKE_SOURCE_DIR})
mkdir build && cd build
cmake ..
cmake --build . -j$(nproc)

Pattern 2: Debug vs release CMake

cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .

cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
strip myapp

Pattern 3: CI build (example)

# GitHub Actions sketch
- name: Build
  run: |
    mkdir build && cd build
    cmake -DCMAKE_BUILD_TYPE=Release ..
    cmake --build . -j$(nproc)

Pattern 4: Static linking (Linux caveat)

g++ -static main.o utils.o -o myapp
# Note: static glibc and some deps can be tricky—validate on target distros

Pattern 5: Inject version macros

g++ -DVERSION=\"$(git describe --tags)\" -DBUILD_DATE=\"$(date -I)\" main.cpp -o main
#include <iostream>
#ifndef VERSION
#define VERSION "unknown"
#endif
int main() {
    std::cout << "Version: " << VERSION << "\n";
    return 0;
}

Pattern 6: Compiler flag map

GoalGCC/ClangMSVC
C++ standard-std=c++17/std:c++17
Warnings-Wall -Wextra/W4
Include path-I/path/I path
Library path-L/path/LIBPATH:path
Link library-lnamename.lib
Debug symbols-g/Zi
Optimize-O2/O2

Pattern 7: Stage isolation for bugs

g++ -E main.cpp -o main.i && head -100 main.i
g++ -S main.cpp -o main.s && cat main.s
g++ -c main.cpp -o main.o && nm main.o
g++ main.o utils.o -o myapp -v

Pattern 8: Unity builds and install rules

Large repos sometimes enable CMAKE_UNITY_BUILD to cut compile units. Use install(TARGETS ...) for packaging.

Pattern 9: Reproducible Docker builds

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y build-essential cmake ninja-build
WORKDIR /app
COPY . .
RUN mkdir build && cd build && cmake -G Ninja .. && ninja

Pattern 10: vcpkg / Conan

find_package(Boost REQUIRED)
target_link_libraries(myapp PRIVATE Boost::system)

Pattern 11: Find compile bottlenecks

g++ -ftime-report -c main.cpp -o main.o
# Clang: -ftime-trace

Pattern 12: Conditional compilation

#ifdef _WIN32
    #define NOMINMAX
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#endif

  • CMake intro — automating multi-file builds
  • Advanced CMake — multi-target projects and dependencies
  • LNK2019 / unresolved external — five common causes

C++ compilation pipeline, preprocess compile link, object files, undefined reference, static vs dynamic libraries, linker errors, four build stages, header include.


Closing

Takeaways

Preprocess: #include, #define. ✅ Compile: AST → assembly. ✅ Assemble: machine code → .o. ✅ Link: .o + libraries → executable.

Heuristic: undefined reference → linking; syntax error → compilation; No such file for headers → preprocessing.

Checklist

  • Inspect stages with -E, -S, -c
  • Use include guards; declarations in headers, definitions in .cpp
  • For undefined reference, verify definitions and link lines
  • Use CMake/Make for correct incremental rebuilds

FAQ (article body)

Q. Where do I start when I see undefined reference?

A. (1) Is the defining .cpp in the build? (2) For external libs, are -l/-L (or CMake targets) correct? (3) For C APIs, is extern "C" used where needed?

Q. Builds are too slow.

A. Parallel builds (-j), ccache, PCH, trim includes, and consider C++20 modules.

More in this series

  • C++ preprocessor guide — #define, #ifdef
  • GDB basics — breakpoints and watchpoints
  • LLDB basics — macOS debugging
  • CMake intro
  • Stack vs heap — recursion and overflow