LNK2019 Unresolved External Symbol in C++: Five Causes

Key takeaways

Fix MSVC LNK2019 and “unresolved external symbol”: missing definitions, .cpp files not in the build, missing .lib links, name mismatches, and templates defined outside headers—with CMake and Visual Studio steps.

Modern CMake: targets & link libraries · generic CMake failures: common CMake errors.

Introduction: the linker error every C++ developer hits

LNK2019: unresolved external symbol is a linker failure: the compiler accepted your translation units, but when linking object files and libraries into an executable or DLL, the linker could not find a definition for a referenced symbol.

Roughly: “there is a declaration, but no matching definition reached the link step.” GCC and Clang report the same situation as undefined reference to 'foo()' (GNU ld) or Undefined symbols for architecture x86_64 (Apple ld). Everything in this article applies to those messages as well.

This article covers:

  • Compile vs link (why the error appears at link time)
  • Five major causes and fixes
  • Visual Studio and CMake checks

Environments: Examples use Visual Studio on Windows and GCC/Clang + CMake on Linux/macOS. The ideas transfer to any toolchain once you map them to your IDE.


What is LNK2019?

1. Compile turns each .cpp into an object file (.obj / .o). The compiler only needs declarations to type-check calls—e.g. void foo(); is enough to compile a call site. It records the call as a reference to an external symbol and moves on.

2. Link merges objects and libraries into the final binary. The linker needs a unique definition for every referenced symbol. If none is found → LNK2019. If two are found, you get the opposite error, LNK2005 “already defined”.

Sample message

error LNK2019: unresolved external symbol "void __cdecl foo(void)" (?foo@@YAXXZ)
referenced in function _main

Meaning: foo is used from main, but no object pulled into the link defines foo. Read both halves. The quoted part is the symbol the linker was looking for, with its full signature and calling convention. The parenthesized part is the decorated name that actually has to match. Two functions that look the same in source can have different decorated names if their parameter types, const qualification, namespace or calling convention differ, and that difference is exactly what causes several of the causes below.

Why two phases?

Incremental builds: change one .cpp, recompile it, relink—faster than compiling everything every time. The price is that each translation unit is compiled in isolation. When main.cpp is compiled, the compiler has no idea whether foo will ever be defined; only the linker sees the whole program. That is why a missing definition can never be reported as a compile error.

flowchart LR
  subgraph compile["Step 1: compile"]
    A[.cpp + headers] --> B[Compiler]
    B --> C[.obj / .o]
    C --> D["Declaration-only can compile"]
  end
  subgraph link["Step 2: link"]
    C --> E[Linker]
    E --> F[.exe]
    E -.->|no definition| G[LNK2019]
  end

Cause 1: declaration but no definition

utils.h

#ifndef UTILS_H
#define UTILS_H
void printMessage();  // declaration only
#endif

main.cpp

#include "utils.h"
int main() {
    printMessage();  // LNK2019 if no TU defines printMessage
    return 0;
}

Fix: add utils.cpp (or define in a TU that is linked):

#include "utils.h"
#include <iostream>
void printMessage() {
    std::cout << "Hello from utils!\n";
}

Build: ensure utils.obj participates in the link (VS: add file to project; CMake: list in add_executable or link a static lib).

Direct build:

g++ main.cpp utils.cpp -o myapp

The same cause has a few less obvious forms. A static data member declared in a class (static int count;) needs exactly one out-of-class definition (int Widget::count = 0;) unless it is declared inline (C++17) or is a constexpr integral constant used only by value. A virtual function that is declared but never defined produces an unresolved reference to the class’s vtable, which MSVC and GCC report in terms of the vtable or the first non-inline virtual function rather than the function you forgot. And a function declared inline in a header but defined in a .cpp is invisible to other translation units, because an inline function must be defined in every TU that uses it.


Cause 2: .cpp omitted from the build

add_executable(myapp
    main.cpp
    # forgot utils.cpp
)

Symptom: build log never compiles utils.cpp.

Fix:

add_executable(myapp
    main.cpp
    utils.cpp
)

In Visual Studio, the equivalent is a file that exists on disk and even shows in the editor but is not part of the project, or that is marked “Excluded From Build” for the current configuration and platform. That last case explains many “works in Debug, fails in Release” reports: the exclusion is set per configuration. In CMake, file(GLOB ...) source lists cause a related problem: a newly added file is not picked up until CMake is re-run, so the build silently compiles the old list.


Cause 3: not linking the library

You included headers and compiled, but the .lib / .a (implementation) is not on the link line. A header tells the compiler that a function exists; it does not bring the code. Windows system APIs are the classic example: calling WSAStartup compiles with <winsock2.h> and then fails with LNK2019 until ws2_32.lib is linked.

Visual Studio: Linker → Input → Additional Dependencies (ws2_32.lib, mylib.lib, …) and Additional Library Directories.

CMake:

target_link_libraries(myapp PRIVATE mylib)

CLI:

g++ main.cpp -o myapp -L/path/to/lib -lmylib

With GNU ld, order matters: libraries are searched once, left to right, and only for symbols that are still unresolved at that point. g++ -lmylib main.cpp therefore fails where g++ main.cpp -lmylib works, and two static libraries that depend on each other may need to be listed twice. The MSVC linker does not have this ordering rule, which is why a build that links on Windows can fail on Linux with undefined reference.

The case that has cost me the most time is a library that is linked but was built for a different configuration: an x86 .lib in an x64 build (the linker reports LNK4272 “library machine type conflicts” and then LNK2019), or a library compiled with a different compiler version or runtime. When the library is clearly on the link line and the symbol still cannot be found, run dumpbin /symbols mylib.lib (or nm -C libmylib.a on Linux) and search for the function name. If it is missing, you have the wrong library; if it is present with a different decorated name, the declaration and the library disagree about the signature.


Cause 4: namespace / name mismatch

Declaration:

namespace util {
    void printMessage();
}

Wrong definition (global):

void printMessage() { }  // different symbol from util::printMessage

Fix: define inside namespace util or as void util::printMessage(). The second form is safer, because a qualified definition must match an existing declaration: if the signature drifts, you get a compile error (“no member function declared”) instead of a new unrelated function and a link error later.

Also watch case and extern “C” for C APIs. A C library exports plain names such as sqlite3_open, while a C++ compiler looks for a mangled name encoding the parameter types. If the header is included from C++ without an extern "C" { ... } block, the reference is mangled and never matches. Well-written C headers add the guard themselves with #ifdef __cplusplus. Other subtle mismatches are a parameter that is const char* in the declaration and char* in the definition, __stdcall versus __cdecl on 32-bit Windows, and wchar_t handled as a native type in one project and as unsigned short in another (the /Zc:wchar_t option).


Cause 5: templates defined only in .cpp

Templates are instantiated where used, and instantiation needs the full definition. If the definition is hidden in a .cpp that does not see the use sites, main.cpp sees only the declaration, emits a reference to add<int>, and trusts that someone else instantiated it. Nobody did, so the linker misses the symbol.

Typical fix: put definitions in the header (or explicit instantiations in .cpp for known types).

// math.h
template <typename T>
T add(T a, T b) { return a + b; }

Explicit instantiation (narrow use):

template int add<int>(int, int);
template double add<double>(double, double);

Explicit instantiation keeps the implementation out of the header and cuts compile times, at the cost of limiting the template to the listed types. Using add<float> then fails at link time again, so this approach suits libraries with a small, fixed set of types.


Visual Studio checklist

  • Linker → Input → Additional Dependencies
  • Linker → General → Additional Library Directories
  • C/C++ → General → Additional Include Directories (compile-time paths only; they never fix a link error)
  • Configuration and Platform drop-downs: settings are per configuration, so check the one that fails
  • Linker → General → Show Progress (/VERBOSE:LIB) to list which libraries the linker actually searched

CMake checklist

add_executable(myapp main.cpp utils.cpp)
target_link_libraries(myapp PRIVATE mylib)

Debug finding:

cmake --build . --verbose

The verbose output shows the exact linker command. Check that every expected object file and library appears on it. If a library target is PRIVATE-linked into another library that myapp uses, the dependency does not propagate to myapp’s headers but is still linked; if the headers of mylib expose symbols from a third library, that dependency should be PUBLIC.


Resolution checklist

CauseCheckFix
No definitionNo TU defines the symbolAdd .cpp / library
Missing TU.cpp not listedAdd to CMake/VS project
Missing libHeaders onlytarget_link_libraries / VS libs
Name mismatchNamespace / spellingAlign declaration & definition
TemplatesDefinition only in .cppHeader definition or explicit inst.

Practical debugging

  1. Read the demangled name in the error (VS shows both).
  2. Follow “referenced in function …” to the call site.
  3. Look for a pattern across all reported symbols. Unlike compile errors, unresolved externals are usually independent of each other, but twenty missing symbols from one library almost always mean that library is not linked at all, while a single missing symbol points to a signature or definition problem.
  4. Match x64/x86 and Debug/Release runtimes with your .lib files.
  5. For C APIs from C++, wrap includes in extern “C” when required.

FAQ (short)

LNK2019 vs LNK1120 — per-symbol vs summary. main unresolved — entry point / subsystem / target type. Header-only — no .lib needed if truly header-only.


Preventing LNK2019 in real projects

  • Prefer qualified out-of-line definitions (void util::f() {}) so signature drift fails at compile time.
  • Verify every .cpp that defines symbols you call is compiled and linked.
  • For third-party SDKs, use their CMake package (find_package + imported targets) or copy the exact library names from their docs instead of guessing.

Closing: LNK2019 means missing definition at link time. Walk definitions → object files → libraries → names → templates, and the fix is usually one configuration step away.