To keep C++ template implementations in a separate source file on macOS, explicitly instantiate the supported types where their definitions are visible, then link that object file into your executable. This walkthrough builds a small local example with Apple’s C++ tools.

The task is specific: compile a C++ program whose template member bodies live in a .cpp file. The example supports two concrete types. A later section explains the additional requirements when a Swift desktop app consumes the same C++ interface.

Why does moving template code into a source file break linking?

A template describes a family of types or functions. Compiling its implementation file does not automatically produce every possible specialization. Class instantiation also does not automatically instantiate every member function body; members are generally instantiated when required. These distinctions explain why a caller can compile successfully yet leave unresolved references at link time. C++ working draft: Implicit instantiation

Consider a header that defines a class template’s layout and declares its constructor and methods. A caller can understand those declarations. But if the method bodies exist only in another source file, the caller cannot use those bodies to generate the required functions. The implementation file must provide the concrete instantiations that the caller needs.

An explicit instantiation definition requests those instantiations. An extern template declaration identifies an instantiation supplied elsewhere. The implementation definitions must be available in the translation unit providing the explicit instantiation definition. C++ working draft: Explicit instantiation

Choose the arrangement according to who controls the type arguments:

Reader’s requirement Suitable arrangement Maintenance consequence
Callers may supply arbitrary suitable types Put definitions in the header, or in an implementation header included from it Callers can instantiate new types
The library supports a known set of types Keep bodies in a source file and explicitly instantiate that set Add an instantiation when adding a supported type
A desktop app needs only a concrete operation Expose an ordinary C++ wrapper around a selected specialization Keep template choices inside the C++ implementation

This decision aid follows from the instantiation rules above. For this walkthrough, the supported set is deliberately limited to int and double.

Build a complete three-file example

Use a new, empty folder and save the following three files together. The synthetic program adds an offset to a value; the small inputs keep the expected results easy to inspect.

Prepare Apple’s command-line tools

Apple supplies the compiler and macOS SDK through Xcode or the standalone Command Line Tools package. If neither is installed, open Terminal and run the following command, then complete the installer dialog. A separate tools installation is unnecessary when Xcode is already installed. Installing the command-line tools

xcode-select --install

Define the public interface

Save this complete header as Offset.hpp. Keep the class definition, including its data member, visible to callers. Only the constructor and method bodies move into the source file.

#ifndef OFFSET_HPP
#define OFFSET_HPP

namespace demo {

template <typename T>
class Offset {
public:
    explicit Offset(T amount);
    T apply(T value) const;

private:
    T amount_;
};

extern template class Offset<int>;
extern template class Offset<double>;

} // namespace demo

#endif

The two extern template lines declare the supplied instantiations; they do not provide the member bodies. A class template definition must remain reachable for this class-level explicit instantiation arrangement. C++ working draft: Explicit instantiation

Define the bodies and provide the instantiations

Save this complete implementation as Offset.cpp. Place the explicit instantiation definitions after the member definitions.

#include "Offset.hpp"

namespace demo {

template <typename T>
Offset<T>::Offset(T amount) : amount_(amount) {}

template <typename T>
T Offset<T>::apply(T value) const {
    return value + amount_;
}

template class Offset<int>;
template class Offset<double>;

} // namespace demo

Order matters: a class-level explicit instantiation definition covers only members defined at that point. It also does not automatically instantiate member templates. Keep one owning source file for each explicit instantiation definition; the language permits at most one per template and argument set in the program. Explicit instantiation, Template instantiation and specialization: General

Call both supported types

Save this complete caller as main.cpp.

#include "Offset.hpp"
#include <iostream>

int main() {
    const demo::Offset<int> whole(3);
    const demo::Offset<double> fractional(0.5);

    const int first = whole.apply(4);
    const double second = fractional.apply(2.0);

    std::cout << first << '\n' << second << '\n';
    return (first == 7 && second == 2.5) ? 0 : 1;
}

The program checks its results through its exit status as well as printing them. This example was inspected for consistency but was not compiled during preparation: the research environment had no C++ compiler or macOS SDK.

Compile both source files, then link both object files

In Terminal, change to the folder containing the three files. Run these commands in order, continuing only when each preceding command succeeds:

xcrun clang++ -std=c++17 -stdlib=libc++ -Wall -Wextra -pedantic -c Offset.cpp -o Offset.o
xcrun clang++ -std=c++17 -stdlib=libc++ -Wall -Wextra -pedantic -c main.cpp -o main.o
xcrun clang++ -std=c++17 -stdlib=libc++ main.o Offset.o -o template-demo
./template-demo

Here, -c produces an object file without linking. The final compiler invocation omits -c and combines both objects into an executable. The commands explicitly select C++17 and libc++ rather than relying on defaults. Clang compiler command guide

The expected output, derived from the synthetic inputs, is:

7
2.5

The essential dependency is visible in the last build command: main.o requests the functions, and Offset.o supplies their instantiations. In a larger project, ensure that the target containing Offset.cpp is built and that its object or library reaches the executable’s link step. A successful compilation of that file alone does not complete the link.

Do not add #include "Offset.cpp" to the caller while also compiling it separately. For this arrangement, include the public header and compile the implementation as its own translation unit. If callers need the bodies instead, deliberately choose the included implementation-header arrangement from the earlier table.

Diagnose missing instantiations in a useful order

First identify the exact unresolved function, including its template arguments and qualifiers. Then follow this sequence. The checks apply the documented distinction between generating object files and linking them, together with the explicit instantiation requirements. Clang compilation stages, C++ explicit instantiation rules

What you find What to check or change
Both supported types have unresolved members Confirm that Offset.o appears in the final link command
A newly used type fails Add its matching instantiation to the owning implementation file and declaration to the header
One method is missing Confirm its body precedes the class instantiation and its declaration matches the definition
A member template is missing Provide the required member-template instantiation separately, with its definition visible
A duplicate-definition diagnostic appears Check for repeated instantiation definitions or an implementation file compiled and included
The diagnostic names an incompatible architecture Check the architecture of every linked object and library

For example, changing the caller to use demo::Offset<float> introduces a type that this implementation does not supply. To support it, add extern template class Offset<float>; inside the header’s namespace and template class Offset<float>; after the bodies in Offset.cpp, then rebuild both objects. Merely adding the declaration does not generate the missing functions.

If LLVM’s separate llvm-nm executable is installed and available on your command path, these optional commands inspect the objects from the example:

llvm-nm --demangle --undefined-only main.o
llvm-nm --demangle --defined-only Offset.o

Look for matching demo::Offset member names in the caller’s unresolved symbols and the implementation’s defined symbols. Other entries are expected, including standard-library references. LLVM documents both filters and name demangling; these commands specifically require llvm-nm, whose availability should not be assumed from an Apple tools installation. llvm-nm command guide

Preserve macOS architecture and ABI compatibility

Explicit instantiation supplies concrete template code; it does not make one object file usable for every architecture. Apple’s universal-binary workflow compiles and links architecture-specific versions before combining them. If the app supports both arm64 and x86_64, include the instantiation source in both builds. Building a universal macOS binary

For a universal build of the sample, inspect the resulting executable with the following command. Check that its reported architectures match the intended build; the earlier ordinary build commands do not request a universal binary.

xcrun lipo -archs ./template-demo

Keeping method bodies separate also leaves the class layout visible in the header. Treat changes to that layout as interface changes requiring dependent code to be rebuilt. Do not infer a stable binary interface from the presence of explicit instantiations. libc++ documents specific compatibility guarantees and configuration limits, and explicitly distinguishes those guarantees from guarantees about user-defined functions. libc++’s ABI Guarantees

What changes when a Swift app calls the C++ code?

Keep two requirements separate: Swift must import a supported C++ declaration, and the final executable must link its implementation. Swift’s documentation exposes concrete class-template specializations through C++ type aliases; the uninstantiated template itself is not directly available as a Swift type. Mixing Swift and C++: Using Class Templates

For the sample interface, the following is an optional header fragment to insert inside namespace demo, after the class definition. It gives the concrete type a name for import:

using IntOffset = Offset<int>;

That alias names the type. The explicit instantiation in Offset.cpp still supplies the out-of-line constructor and method. Swift imports C++ headers using Clang modules and currently cannot import C++20 modules. Follow the header-based interoperability setup for the project. Mixing Swift and C++

In Xcode, enable C++ and Objective-C interoperability with the value C++ / Objective-C++ for the Swift target. For an app target using C++ from that same target, include the C++ header in its bridging header. Framework targets expose supported declarations through public headers. Setting Up Mixed-Language Swift and C++ Projects

Mixed Swift and C++ code must use the same C++ standard library; Apple platforms use libc++. Interoperability requires C++14 or newer. Match the imported headers’ language mode to the C++ build, and check the supported-feature list before exposing more elaborate templates. Explicit instantiation does not extend Swift’s supported language mappings. Supported Features and Constraints of C++ Interoperability

For a fixed set of types, maintain the public declarations, the instantiation list, and the final link inputs together. When a new caller needs another type, either add that type deliberately or make the implementation available for caller-side instantiation. That choice keeps the source-file boundary understandable as the desktop project grows.

Sources