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, .cpp not 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.

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 .cpp is 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

ScenarioFix
New library, unresolved symbols from itfind_package + target_link_libraries with the library’s imported target
multiple definitionNo non-inline function definitions in headers included by multiple TUs; use inline, one .cpp, or static
cannot find -lfoo / find_package failsCMAKE_TOOLCHAIN_FILE (vcpkg), CMAKE_PREFIX_PATH
C library from C++extern "C" + link the C library
Subproject static libadd_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. If template <typename T> T clamp(T) is only defined in utils.cpp, main.cpp sees just the declaration, emits a call to clamp<int>, and nothing ever instantiates it. Define templates in headers, or add explicit instantiations (template int clamp<int>(int);) in the .cpp for 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 .cpp is in the target.

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_32 on Windows for sockets.
  • Prefer imported targets (fmt::fmt, Boost::filesystem) over raw -lfoo flags.

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.


  • GNU ld processes static libraries left to right and only pulls in objects that resolve symbols needed so far. If libapp.a needs something from libutil.a, libutil.a must 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 -l flags 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

  1. Extract the symbol and its full signature from the error.
  2. grep / IDE: find the declaration and the definition, and compare signatures, namespaces and const exactly.
  3. Confirm the defining .cpp is in the CMake target.
  4. Check what the library actually exports: nm -C libfoo.a | grep parse on Linux, dumpbin /SYMBOLS foo.lib or dumpbin /EXPORTS foo.dll on Windows. A U in nm output means “needed but undefined here”; T means “defined here”.
  5. Print the real link line with cmake --build build --verbose and check that the library appears, and after the libraries that need it.
  6. cmake --graphviz=deps.dot build draws 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 patternCheck
pthread_*Threads::Threads
dlopenCMAKE_DL_LIBS
_main / WinMainsubsystem 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_v0linking C++ objects with the C driver: use g++, or project(... LANGUAGES CXX)
symbols with __cxx11 in the nameobjects 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.


SymptomLikely causeFix
undefined referenceMissing definition, lib, or signature mismatchAdd .cpp / target_link_libraries / match signatures
cannot find -lfooPaths / toolchainvcpkg, find_library, PREFIX_PATH
C symbols missingManglingextern "C"
multiple definitionHeader definitionsinline / single .cpp
LNK2038 / __cxx11 mismatchesABI or runtime mismatchRebuild all with the same settings

Next: Asio deadlock debugging (#49-3) Previous: Segfault debugging (#49-1)