Lesson 5 of 10
Linker errors read badly. They arrive after a successful compile, they name symbols rather than lines, and half the text is mangled. They are also, in practice, a very short list — five shapes cover almost everything you will meet.
The question a linker error always answers is the same: someone said this exists, and I could not find it. Your job is to work out who said it and where it should have been.
1. Undefined reference: no definition anywhere
The basic case from lesson 17.1. A declaration promised something; nothing delivered.
// counter.hpp
#pragma once
struct Counter {
static int total; // declaration only
static void bump(); // declaration only
};
// m2.cpp
#include "counter.hpp"
int main() { Counter::bump(); return Counter::total; }
g++ -std=c++20 -c m2.cpp -o m2.o && echo "compiled fine"
g++ m2.o -o p2 2>&1 | grep -v 'warning:'
compiled fine
/usr/bin/ld: m2.o: in function `main':
m2.cpp:(.text+0x9): undefined reference to `Counter::bump()'
/usr/bin/ld: m2.cpp:(.text+0xf): undefined reference to `Counter::total'
collect2: error: ld returned 1 exit status
A static data member is a declaration, even though it looks like a definition and sits inside a
class body. It needs int Counter::total = 0; in exactly one .cpp — or, since C++17, inline static int total = 0; in the class, which is nearly always what you want now.
2. Undefined reference to vtable
This one is worth recognising on sight, because the error does not name the thing you got wrong:
// shape.hpp
#pragma once
struct Shape {
virtual ~Shape() = default;
virtual double area() const; // declared, never defined
};
// main.cpp
#include "shape.hpp"
struct Circle : Shape {
double area() const override { return 3.14; }
};
int main() { Circle c; return static_cast<int>(c.area()); }
g++ -std=c++20 -c main.cpp -o main.o && echo "compiled fine"
g++ main.o -o prog 2>&1 | grep -v 'warning:'
compiled fine
/usr/bin/ld: main.o: in function `Shape::~Shape()':
main.cpp:(.text._ZN5ShapeD2Ev[_ZN5ShapeD5Ev]+0xf): undefined reference to `vtable for Shape'
/usr/bin/ld: main.o:(.data.rel.ro._ZTI6Circle[_ZTI6Circle]+0x10): undefined reference to `typeinfo for Shape'
collect2: error: ld returned 1 exit status
undefined reference to 'vtable for Shape' means: you declared a virtual function in Shape and
never defined it.
The mechanism is a GCC implementation rule. A class's vtable is emitted alongside the definition of
its first non-inline, non-pure virtual function — the key function. Declare one and never define
it, and there is no key function, so no vtable, so anything that needs Shape's type information
fails. That includes Circle, which never touched Shape::area at all.
The three fixes, in order of how often they are right:
- The function was meant to be pure:
virtual double area() const = 0; - The function was meant to be defined: write the body.
- The function was meant to be inline: define it in the class.
Note the error points at ~Shape() and at typeinfo for Shape, not at area. Do not chase those.
Go and look at the virtual functions of the class named in the message.
3. Undefined reference across a language boundary
// clib.c
int c_add(int a, int b) { return a + b; }
// m3.cpp
#include <iostream>
int c_add(int a, int b); // no extern "C": C++ mangling assumed
int main() { std::cout << c_add(2, 3) << "\n"; }
gcc -c clib.c -o clib.o
g++ -std=c++20 -c m3.cpp -o m3.o
g++ m3.o clib.o -o p3 2>&1 | grep -v 'warning:'
echo "--- what each object file has to say about c_add:"
nm clib.o | grep add
nm m3.o | grep add
/usr/bin/ld: m3.o: in function `main':
m3.cpp:(.text+0x13): undefined reference to `c_add(int, int)'
collect2: error: ld returned 1 exit status
--- what each object file has to say about c_add:
0000000000000000 T c_add
U _Z5c_addii
Two lines of nm give the whole diagnosis. The C file defines c_add; the C++ file needs
_Z5c_addii. They are not the same symbol, so they do not match.
C has no overloading and therefore no mangling. Declaring a C function from C++ requires saying so:
// m3.cpp
#include <iostream>
extern "C" int c_add(int a, int b);
int main() { std::cout << c_add(2, 3) << "\n"; }
g++ -std=c++20 -c m3.cpp -o m3.o
g++ m3.o clib.o -o p3 && ./p3
5
A C header meant to be usable from C++ wraps its declarations in the standard idiom, so callers do not have to remember:
// clib.h
#ifndef CLIB_H
#define CLIB_H
#ifdef __cplusplus
extern "C" {
#endif
int c_add(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
4. Undefined reference to a template instantiation
This one catches everybody once, and the rule behind it is not obvious.
// stack.hpp
#pragma once
#include <vector>
template <typename T>
class Stack {
std::vector<T> items_;
public:
void push(T v); // declared in the header
T pop(); // definition lives in stack.cpp
bool empty() const { return items_.empty(); }
};
// stack.cpp
#include "stack.hpp"
template <typename T>
void Stack<T>::push(T v) { items_.push_back(std::move(v)); }
template <typename T>
T Stack<T>::pop() {
T v = std::move(items_.back());
items_.pop_back();
return v;
}
// main.cpp
#include <iostream>
#include "stack.hpp"
int main() {
Stack<int> s;
s.push(42);
std::cout << s.pop() << "\n";
}
Both files compile. Neither is wrong on its own. And:
g++ -std=c++20 -c stack.cpp -o stack.o
g++ -std=c++20 -c main.cpp -o main.o
g++ main.o stack.o -o prog 2>&1 | grep -v 'warning:'
echo "--- what stack.o actually defines:"
nm -C stack.o | grep Stack | head -5
echo "(nothing, which is the whole problem)"
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x36): undefined reference to `Stack<int>::push(int)'
/usr/bin/ld: main.cpp:(.text+0x42): undefined reference to `Stack<int>::pop()'
collect2: error: ld returned 1 exit status
--- what stack.o actually defines:
(nothing, which is the whole problem)
stack.o contains no Stack code at all. A template is not code; it is instructions for
generating code. stack.cpp never mentions Stack<int>, so the compiler had no reason to generate
anything, and it did not.
Two fixes. The usual one is to put template definitions in the header, so every translation unit
that uses Stack<int> can generate it. That is why the standard library is almost entirely headers,
and it is the reason lesson 17.3's measurements look the way they do.
The other is explicit instantiation — telling the compiler which types to generate, in the
.cpp:
// stack.cpp
#include "stack.hpp"
template <typename T>
void Stack<T>::push(T v) { items_.push_back(std::move(v)); }
template <typename T>
T Stack<T>::pop() {
T v = std::move(items_.back());
items_.pop_back();
return v;
}
// Explicit instantiation: generate the code for these types, here, now.
template class Stack<int>;
template class Stack<double>;
g++ -std=c++20 -c stack.cpp -o stack.o
g++ main.o stack.o -o prog && ./prog
echo "--- stack.o now defines:"
nm -C stack.o | grep -E ' [TW] ' | grep Stack | sort -k3
42
--- stack.o now defines:
0000000000000000 W Stack<double>::empty() const
0000000000000000 W Stack<double>::pop()
0000000000000000 W Stack<double>::push(double)
0000000000000000 W Stack<int>::empty() const
0000000000000000 W Stack<int>::pop()
0000000000000000 W Stack<int>::push(int)
This keeps the template's body out of the header, which is a real compile-time win — at the price of a fixed menu. Use a type you did not list and you are back where you started:
// m4.cpp
#include <string>
#include "stack.hpp"
int main() { Stack<std::string> s; s.push("x"); return s.empty(); }
g++ -std=c++20 -c m4.cpp -o m4.o
g++ m4.o stack.o -o p4 2>&1 | grep -v 'warning:' | head -3
/usr/bin/ld: m4.o: in function `main':
m4.cpp:(.text+0x5c): undefined reference to `Stack<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >::push(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: error: ld returned 1 exit status
So: header definitions for a general-purpose template, explicit instantiation when the set of types is genuinely closed and the header is genuinely expensive.
5. Multiple definition
Covered in lesson 17.2 — a non-inline definition in a header, included twice. The message names
both object files, and the fix is inline, or moving the definition to a .cpp.
The three commands
Almost every linker investigation is one of these.
nm -C file.o — what does this object file define and need? Uppercase is defined, U is
needed, -C demangles.
nm -C -u file.o — just the undefined ones. This is the file's dependency list:
nm -C -u main.o | head -6
U _Unwind_Resume
U Stack<int>::pop()
U Stack<int>::push(int)
U std::ostream::operator<<(int)
U std::ios_base_library_init()
U std::cout
_Unwind_Resume and std::cout come from the standard library and will be resolved automatically.
Stack<int>::push(int) is the one you have to supply.
c++filt — demangle a name you copied out of an error message:
echo "_Z5c_addii" | c++filt
echo "_ZNK5Stack IiE5emptyEv" | tr -d ' ' | c++filt
c_add(int, int)
Stack<int>::empty() const
Keep c++filt in mind for the cases where the linker prints a raw symbol — with static libraries
and stripped binaries it often does.
A method
When a link fails, in order:
- Read which symbol is missing, demangled. That is the only real information in the message.
- Decide whether it is yours or a library's. Yours → you forgot to define it, or forgot to add
the
.oto the link line. A library's → the library is missing from the link line, or is in the wrong position (lesson 17.6). - Check it is actually defined somewhere:
nm -C *.o | grep <name>. No match means it was never compiled; aUeverywhere and no uppercase means every file expects someone else to have it. - If the name looks right but does not match, compare the mangled forms. Signature mismatch, a
missing
const, and a missingextern "C"all look identical until you do.
Step 3 catches more real bugs than the other three together, usually in the form of a .cpp that
nobody added to the build.
Try it
Produce each of the five errors deliberately, in one project, and write the one-line diagnosis for
each from its message alone. Then fix them one at a time and confirm with nm at each step.
Checkpoint
- A linker error always means: something was declared, and no definition was found.
- Static data members declared in a class need an out-of-class definition — or
inline static. undefined reference to 'vtable for X'means a virtual function ofXis declared and never defined. Make it pure, define it, or inline it.- The vtable error points at the destructor and at
typeinfo; ignore both and look at the virtuals. - A missing
extern "C"makes C++ look for a mangled name that the C object file does not have. - Wrap C headers in
#ifdef __cplusplus / extern "C" { ... }so callers cannot get it wrong. - A template is not code. A
.cppholding template definitions generates nothing unless it uses the type. - Fix with header definitions, or
template class Stack<int>;explicit instantiation for a closed set of types. - Multiple definition is a non-
inlinedefinition in a header; the message names both files. nm -C file.oshows what a file defines and needs;nm -C -ushows just the needs.c++filtdemangles a symbol copied out of an error message.- The highest-yield check is step 3:
nm -C *.o | grep <name>. No match usually means a.cppwas never added to the build.
Questions about this lesson
Log in to ask a question.