Templates in Multiple Files

When working with regular functions, you typically separate declarations (in header files) from definitions (in source files). But templates require special handling.

Why Templates Are Different

Consider this attempt to use a template across multiple files:

calculation.h:

#pragma once

template <typename T>
T doubleValue(T number);

calculation.cpp:

template <typename T>
T doubleValue(T number)
{
    return number * 2;
}

main.cpp:

#include "calculation.h"
#include <iostream>

int main()
{
    std::cout << doubleValue(5) << '\n';
    std::cout << doubleValue(3.7) << '\n';

    return 0;
}

This looks reasonable, but it won't link. You'll get errors like:

undefined reference to `int doubleValue<int>(int)`
undefined reference to `double doubleValue<double>(double)`

Understanding the Problem

When the compiler processes main.cpp:

  1. It sees the forward declaration of doubleValue from the header
  2. It encounters calls to doubleValue<int> and doubleValue<double>
  3. It needs the template definition to instantiate these functions
  4. It doesn't have the definition, so it assumes they'll be linked later

When the compiler processes calculation.cpp:

  1. It sees the template definition
  2. There are no calls to the template in this file
  3. The compiler doesn't instantiate any functions

Result: The linker can't find the instantiated functions because they were never created.

Solution: Put Templates in Headers

The standard solution is to put the entire template definition in the header file:

calculation.h:

#pragma once

template <typename T>
T doubleValue(T number)
{
    return number * 2;
}

main.cpp:

#include "calculation.h"
#include <iostream>

int main()
{
    std::cout << doubleValue(5) << '\n';
    std::cout << doubleValue(3.7) << '\n';

    return 0;
}

Now when the compiler processes main.cpp, it has the complete template definition available. It can instantiate doubleValue<int> and doubleValue<double> right there.

Key Concept
Templates need their full definitions visible at the point of use. This is why template code typically lives entirely in header files.

What About Code Duplication?

You might wonder: if multiple source files include the same template header, won't the compiler generate duplicate functions?

Yes and no. The compiler will instantiate the template in each translation unit that uses it. However, template instantiations are implicitly inline, which tells the linker that duplicate definitions are okay—it keeps one copy and discards the rest.

Organizing Larger Templates

For complex templates with many functions, putting everything in one header can be unwieldy. A common pattern uses .tpp or .inl files for implementations:

buffer.h:

#pragma once

template <typename T, int Size>
class CircularBuffer
{
public:
    void push(T value);
    T pop();
    bool isEmpty() const;

private:
    T data[Size]{};
    int head{};
    int tail{};
    int count{};
};

#include "buffer.tpp"  // Include implementation at end

buffer.tpp:

template <typename T, int Size>
void CircularBuffer<T, Size>::push(T value)
{
    data[tail] = value;
    tail = (tail + 1) % Size;
    ++count;
}

template <typename T, int Size>
T CircularBuffer<T, Size>::pop()
{
    T value{ data[head] };
    head = (head + 1) % Size;
    --count;
    return value;
}

template <typename T, int Size>
bool CircularBuffer<T, Size>::isEmpty() const
{
    return count == 0;
}

main.cpp:

#include "buffer.h"

int main()
{
    CircularBuffer<int, 5> buffer{};
    buffer.push(10);
    buffer.push(20);

    return 0;
}

The .tpp file contains the implementation details, but it's included by the header, so everything is still available at the point of use.

Alternative: Explicit Instantiation (Advanced)

For certain specialized use cases, you can explicitly instantiate specific template versions:

calculation.cpp:

template <typename T>
T doubleValue(T number)
{
    return number * 2;
}

// Explicitly instantiate specific versions
template int doubleValue<int>(int);
template double doubleValue<double>(double);

This tells the compiler to generate these specific versions in this translation unit. Other files can use forward declarations to reference them.

However, this approach has downsides:

  • You must know all needed instantiations in advance
  • Adding new types requires updating the explicit instantiation list
  • It's fragile and error-prone
Best Practice
For function templates: Put the entire definition in the header file.

For large class templates: Consider using a separate .tpp or .inl file for implementations, but include it at the end of the header.

Avoid: Separating templates across .h and .cpp files without explicit instantiation.

This approach might feel wasteful compared to regular functions, but it's necessary for templates to work correctly. The compiler needs the complete template definition to generate code for each type you use it with.

Summary

Templates require special consideration when organizing code across multiple files:

The Problem:

  • Templates need full definitions to instantiate
  • Separating declaration (.h) and definition (.cpp) causes linker errors
  • The compiler can't instantiate what it can't see

Solutions:

  1. Put templates in headers - Most common and recommended approach
  2. Use .tpp/.inl files - For organization, include at end of header
  3. Explicit instantiation - Advanced, fragile, avoid unless necessary

Why It Works:

  • Template instantiations are implicitly inline
  • Linker handles duplicate definitions automatically
  • Each translation unit gets what it needs

Key Rules:

  • Template definitions must be visible at point of use
  • Header-only is the standard approach for templates
  • Don't split templates across .h and .cpp files