CMake Link Errors: LNK2019, undefined reference, and Fixes
Introduction: “undefined reference to ...” / LNK2019
undefined reference to ‘symbol’ (GCC/Clang) and LNK2019: unresolved external symbol (MSVC) mean the linker could not find a definition for a referenced symbol. Compile succeeds because declarations suffice; link fails when definitions are missing or wrong libraries are linked.
The key to these errors is noticing which phase failed. If the message comes from the compiler (error: 'foo' was not declared in this scope), a header or declaration is missing. If it comes from ld, lld, link.exe or collect2, every file compiled fine and the problem is that the pieces do not fit together: a definition was never compiled, never handed to the linker, compiled with a different signature, or compiled as C instead of C++. None of those can be fixed by adding #include lines, which is the first thing people usually try.
This article covers:
- Missing definitions, wrong translation units,
.cppnot in target - Missing target_link_libraries
find_package/ library not found (vcpkg toolchain,CMAKE_PREFIX_PATH)- C vs C++: extern “C”
- Link order and multiple definition
- CMake-centric debugging steps and CI hygiene
See also: CMake intro, Compilation pipeline, Advanced CMake.
Compile vs link
flowchart LR
subgraph Compile[Compile]
A[.cpp] --> B[Compiler]
B --> C[.o / .obj]
end
subgraph Link[Link]
C --> D[Linker]
E[.a / .so / .lib] --> D
D --> F[Executable / DLL]
end
- Compile: each
.cppis independent; declarations are enough. - Link: the linker resolves symbols across objects and libraries.
Each object file carries a list of symbols it defines and a list it needs. The linker’s job is to match every “needs” with exactly one “defines”. An unmatched need is undefined reference / LNK2019; two definitions for the same name is multiple definition / LNK2005. Everything below is a variation on those two outcomes.
Scenarios
| Scenario | Fix |
|---|---|
| New library, unresolved symbols from it | find_package + target_link_libraries with the library’s imported target |
multiple definition | No non-inline function definitions in headers included by multiple TUs; use inline, one .cpp, or static |
cannot find -lfoo / find_package fails | CMAKE_TOOLCHAIN_FILE (vcpkg), CMAKE_PREFIX_PATH |
| C library from C++ | extern "C" + link the C library |
| Subproject static lib | add_library + target_link_libraries(app PRIVATE mylib) |
Debugging flow
flowchart TB
A[Link error] --> B{Type?}
B -->|undefined| C[Symbol name → own code or lib?]
C -->|Own| D[Add .cpp / target_sources]
C -->|External| E[target_link_libraries]
B -->|multiple def| F[Header definitions / ODR]
B -->|not found| G[Toolchain / prefix path]
Reading error messages
- GCC/Clang:
undefined reference to 'Qualified::Name(args)', printed with the demangled signature and the object file that needed it. - MSVC:
LNK2019: unresolved external symbol "public: void __cdecl Parser::parse(class std::basic_string<...> const &)" (?parse@Parser@@...) referenced in function main. The part in quotes is the demangled signature; the part in parentheses is the mangled name.
Read the signature carefully, because the most common “but it is defined” case is a mismatch you can only see there. void parse(const std::string&) declared in the header and void parse(std::string&) defined in the .cpp are two different functions to the linker. So are a member function defined as a free function (void parse(...) instead of void Parser::parse(...)), a definition inside a different namespace, and a const member function defined without const. The compiler cannot catch these, because the .cpp legitimately defines a function; it just is not the one the header promised.
Missing definition / .cpp not built
Add the implementation .cpp to add_executable / add_library or target_sources. If logic lives in another static library, link that library to the executable.
This happens constantly after adding a new file: the IDE shows it in the project tree, but CMake only compiles files listed in the target. With file(GLOB ...) the new file is only picked up after CMake re-runs, which is one reason CMake’s documentation discourages globbing for sources; adding CONFIGURE_DEPENDS to the glob makes the build re-check it.
Two special cases produce the same error with a perfectly listed .cpp:
- Templates defined in a
.cpp. A template is compiled when it is used with concrete types, and at that point the compiler needs its body. Iftemplate <typename T> T clamp(T)is only defined inutils.cpp,main.cppsees just the declaration, emits a call toclamp<int>, and nothing ever instantiates it. Define templates in headers, or add explicit instantiations (template int clamp<int>(int);) in the.cppfor the types you support. undefined reference to 'vtable for Widget'. GCC and Clang emit a class’s vtable in the object file that defines its key function: the first virtual function that is neither inline nor pure. If that function, often the destructor, is declared but never defined, no vtable is emitted anywhere, and the error names the vtable instead of the missing function. Define the function, and check that its.cppis in the target.
Missing library link
find_package(Threads REQUIRED)
target_link_libraries(my_app PRIVATE Threads::Threads)
- find_package(Threads REQUIRED) → target_link_libraries(… Threads::Threads)
- Header-only libraries such as Boost.Asio need no library of their own, but may need what they depend on: Threads on Linux, and
ws2_32on Windows for sockets. - Prefer imported targets (
fmt::fmt,Boost::filesystem) over raw-lfooflags.
Imported targets carry their include directories, compile definitions and transitive dependencies with them. Linking fmt::fmt adds the header path and the library in one step, and if a library itself needs pthread or dl, its target adds that too. With raw -l flags you have to know and maintain that list yourself, which is how builds end up working on one machine and failing on another. The keyword matters as well: PRIVATE links the library into this target only, while PUBLIC also passes it on to targets that link to this one. A static library that uses fmt in its headers but links it PRIVATE makes consumers fail with undefined references to fmt symbols.
Library not found
vcpkg:
vcpkg install fmt:x64-linux
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
find_package(fmt REQUIRED)
target_link_libraries(my_app PRIVATE fmt::fmt)
Always target_link_libraries after find_package—headers alone are not enough.
The toolchain file must be passed on the first configure of a build directory; CMake caches the toolchain, so adding -DCMAKE_TOOLCHAIN_FILE to an existing build directory is silently ignored. Delete the build directory (or use a fresh one) when switching. If find_package itself fails, the error lists the file names it looked for (fmtConfig.cmake, fmt-config.cmake); pointing CMAKE_PREFIX_PATH at the library’s install prefix, or setting fmt_DIR to the directory containing that file, tells CMake where to look. cannot find -lfoo from the linker is a different problem: CMake handed the linker a library name without a valid path, which usually means a hard-coded -lfoo rather than an imported target.
extern "C" for C libraries
C++ name mangling does not match C export names. Wrap C declarations in extern “C” (or use headers that already do).
// legacy_c.h — make a C header usable from both languages
#ifdef __cplusplus
extern "C" {
#endif
int legacy_init(const char* config);
#ifdef __cplusplus
}
#endif
A C++ compiler encodes parameter types into symbol names so that overloads can coexist; legacy_init(const char*) becomes something like _Z11legacy_initPKc. A C compiler exports the plain name legacy_init. Without extern "C", C++ callers look for the mangled name and the linker reports undefined reference to 'legacy_init(char const*)'; the parameter list in the message is the giveaway that it was looking for a C++ symbol. The reverse case appears when a .c file is compiled as C++ because it was renamed to .cpp, or when project() did not enable the C language: then your definitions get mangled and C code cannot find them.
Link order and multiple definition
- GNU ld processes static libraries left to right and only pulls in objects that resolve symbols needed so far. If
libapp.aneeds something fromlibutil.a,libutil.amust come after it on the command line. lld and the MSVC linker are not sensitive to this. - CMake computes the order from target dependencies, so declaring them correctly (
target_link_libraries(app_core PRIVATE util)) fixes order problems; manual-lflags and circular dependencies between static libraries are where it breaks. - ODR: one definition per symbol across the program; inline / single
.cpp/ static in TU.
multiple definition of 'helper()' (MSVC: LNK2005 ... already defined in main.obj) almost always means a function or a global variable is defined in a header that several .cpp files include. Each translation unit then contributes its own copy. Marking the function inline, or the variable inline (C++17), tells the linker the copies are the same entity. For global variables, the other fix is extern int counter; in the header and one int counter = 0; in a .cpp.
Example fixes (sketches)
Missing utils.cpp:
add_executable(app main.cpp utils.cpp)
Boost with compiled components:
find_package(Boost REQUIRED COMPONENTS filesystem)
target_link_libraries(app PRIVATE Boost::filesystem)
Boost.Asio and Boost.System are header-only in current Boost releases (Boost.System since 1.69), so they need Boost::headers rather than a compiled component; only libraries such as filesystem, regex or program_options have real binaries to link.
Multiple definition from header:
inline int add(int a, int b) { return a + b; } // or move definition to .cpp
vcpkg fmt:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=.../vcpkg.cmake
Debugging steps
- Extract the symbol and its full signature from the error.
- grep / IDE: find the declaration and the definition, and compare signatures, namespaces and
constexactly. - Confirm the defining .cpp is in the CMake target.
- Check what the library actually exports:
nm -C libfoo.a | grep parseon Linux,dumpbin /SYMBOLS foo.libordumpbin /EXPORTS foo.dllon Windows. AUinnmoutput means “needed but undefined here”;Tmeans “defined here”. - Print the real link line with
cmake --build build --verboseand check that the library appears, and after the libraries that need it. cmake --graphviz=deps.dot builddraws the target dependency graph if the problem is a missing or circular dependency.
When I inherit a build with a link error nobody can explain, step 5 settles it most often: the library everyone assumes is linked is simply absent from the command, or it is there, but a different version from another prefix is being picked up first.
Quick fixes table
| Error pattern | Check |
|---|---|
pthread_* | Threads::Threads |
dlopen | CMAKE_DL_LIBS |
_main / WinMain | subsystem and entry point (WIN32 in add_executable means WinMain) |
vtable for ... | key function (first non-inline virtual) defined, .cpp linked |
__atomic_* | libatomic where needed |
__gxx_personality_v0 | linking C++ objects with the C driver: use g++, or project(... LANGUAGES CXX) |
symbols with __cxx11 in the name | objects built with different _GLIBCXX_USE_CXX11_ABI settings |
MSVC LNK2038: mismatch detected for 'RuntimeLibrary' | all libraries must use the same /MD vs /MT and Debug vs Release runtime |
The last two rows are ABI mismatches: the symbol exists, but compiled under different rules. With libstdc++, a library built with the old string ABI exports std::string functions whose mangled names lack __cxx11, and code built with the new ABI cannot link against them. On Windows, mixing a Debug build of your app with a Release build of a library (or /MT with /MD) triggers LNK2038, and the only real fix is to rebuild everything with matching settings, which CMAKE_MSVC_RUNTIME_LIBRARY and a package manager triplet make consistent.
Production patterns
- IMPORTED / INTERFACE targets encapsulate paths.
- FetchContent pins versions.
- CI uses same vcpkg toolchain as dev.
- PUBLIC/PRIVATE propagation for includes and libs.
The common thread is that link settings belong to targets, not to the global build. Global link_libraries() or CMAKE_EXE_LINKER_FLAGS hacks make every target link everything and hide which one actually needs a library, so the first refactor that splits a target brings the undefined references back.
Link error symptoms, causes and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
undefined reference | Missing definition, lib, or signature mismatch | Add .cpp / target_link_libraries / match signatures |
cannot find -lfoo | Paths / toolchain | vcpkg, find_library, PREFIX_PATH |
| C symbols missing | Mangling | extern "C" |
multiple definition | Header definitions | inline / single .cpp |
LNK2038 / __cxx11 mismatches | ABI or runtime mismatch | Rebuild all with the same settings |
Next: Asio deadlock debugging (#49-3) Previous: Segfault debugging (#49-1)