Templates and Header Files

Learn how to separate class templates into declarations and definitions while avoiding common linker errors
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated

Previously, we’ve seen how we can split a class and its members into separate declarations and definitions:

// Resource.h
#pragma once
#include <iostream>

class Resource {
 public:
  int Value;
  
  // Declaration
  void Log();
};

// Definition
void Resource::Log() {
  std::cout << "Resource Value: " << Value;
}

This additionally allows us to split these components across two files - a header file and an implementation file - which can be helpful for project organisation and keeping compile times fast.

In this lesson, we’ll see how to use this technique when we’re creating a class template rather than a basic class. We’ll encounter a linker problem that can crop up in this context, and our options for working around it.

Separating Class Template Implementations

Let’s say we have the following template class, which declares a Log() function, but hasn’t implemented it:

// Resource.h
#pragma once

template <typename T>
class Resource {
 public:
  T Value;
  void Log();
};

Implementing a member function of a template class broadly follows the same idea as implementing a member function of a regular class. We just have some additional template-based syntax added above the function and to its signature:

// Resource.h
#pragma once
#include <iostream>

class Resource {/*...*/}; template <typename T> void Resource<T>::Log() { std::cout << "Resource Value: " << Value; }

The need for the additional syntax here is because class templates can be used to create any number of classes, and we can provide different implementations of their member functions depending on the specific template arguments. We’ll see examples of this later in the chapter.

Instantiating our template and then creating an object of that type confirms we have the expected behaviour:

// main.cpp
#include "Resource.h"

int main() {
  Resource<int> Object{42};
  Object.Log();
}
Resource Value: 42

A slightly more complex example, where our class template has multiple parameters, might look like this:

// Pair.h
#pragma once

template <typename T1, typename T2>
class Pair {
 public:
  Pair(T1 first, T2 second)
    : mFirst(first), mSecond{second} {}

  // Declarations
  T1 GetFirst() const;
  T2 GetSecond() const;

 private:
  T1 mFirst;
  T2 mSecond;
};

// Definitions
template <typename T1, typename T2>
T1 Pair<T1, T2>::GetFirst() const {
  return mFirst;
}

template <typename T1, typename T2>
T2 Pair<T1, T2>::GetSecond() const {
  return mSecond;
}

One of the main reasons we separate declaration and definition in this way is that it allows our definition to be in a different file entirely. For example, we’d often have all our declarations in a header file whilst definitions are in a separate implementation file.

If we try to do this with templates, we can run into some problems.

Moving Implementations to a Different File

Let’s split our Pair class template across a header file and implementation file, in much the same way we’ve done with regular classes in the past:

// Pair.h
#pragma once

template <typename T1, typename T2>
class Pair {
 public:
  Pair(T1 first, T2 second)
    : mFirst(first), mSecond{second} {}

  // Declarations
  T1 GetFirst() const;
  T2 GetSecond() const;

 private:
  T1 mFirst;
  T2 mSecond;
};
// Pair.cpp
#include "Pair.h"

// Definitions
template <typename T1, typename T2>
T1 Pair<T1, T2>::GetFirst() const {
  return mFirst;
}

template <typename T1, typename T2>
T2 Pair<T1, T2>::GetSecond() const {
  return mSecond;
}

In isolation, this will compile. However, once we try to use our member functions in some other source file, we’ll encounter linker errors:

// main.cpp
#include <iostream>
#include "Pair.h"

int main() {
  Pair<int, bool> MyPair{42, true};

  std::cout << "First: " <<  MyPair.GetFirst();
}
error LNK2019: unresolved external symbol Pair<int,bool>::GetFirst() referenced in function main
fatal error LNK1120: 1 unresolved externals

Because templates are instantiated at the compilation step (i.e., before linking), our program cannot compile in this form. Our main.cpp source file is trying to create a class from our Pair template but it doesn’t have the implementation, so it can’t.

Our Pair.cpp file has the implementation so it could create a Pair<int, bool>, but it doesn’t know it needs to. The invocation of the template with that particular set of arguments is in another source file.

Our solutions to solve this problem therefore fall into two categories:

  1. Provide the source files that use the template access to its implementation
  2. Give the template’s implementation the required arguments

Option 1: Provide the Implementation

Variations of this option involve simply providing the implementation within the header that is included by files that use our template. For example, we can just provide implementations at the same time we declare the methods:

// Pair.h
#pragma once

template <typename T1, typename T2>
class Pair {
 public:
  Pair(T1 first, T2 second)
    : mFirst(first), mSecond{second} {}

  T1 GetFirst() const {
    return mFirst;
  };
  
  T2 GetSecond() const {
    return mSecond;
  };

 private:
  T1 mFirst;
  T2 mSecond;
};

We could alternatively provide declarations separately, but within the same header file:

// Pair.h
#pragma once

class Pair {/*...*/}; template <typename T1, typename T2> T1 Pair<T1, T2>::GetFirst() const { return mFirst; } template <typename T1, typename T2> T2 Pair<T1, T2>::GetSecond() const { return mSecond; }

A variation of this idea involves us providing declarations in a different file, but then including that file at the bottom of our header. By convention, we give files that provide implementations for templates .ipp or .tpp extensions:

// Pair.h
#pragma once

class Pair {/*...*/}; #include "Pair.ipp"
// Pair.ipp
#pragma once
#include "Pair.h"

Option 2: Provide the Arguments

If we know what instances of our template are going to be required elsewhere in our code, we can proactively create those instances within the files that implement our template.

The syntax looks like the following:

// Pair.cpp
#include "Pair.h"

template class Pair<int, bool>; template class Pair<bool, bool>; template class Pair<float, int>;

After compiling this code, the Pair<int, bool>, Pair<bool, bool> and Pair<float, int> types will be created, and available to other source files through the linker. If we find ourselves needing a different type, we can return to this file and add another instantiation.

In practice, providing arguments in this way is less common than simply providing the full implementation of our template in the header file. It is particularly impractical if we’re writing library code to be used by other developers, as we have no way of knowing what types their project is going to be using with our template.

Summary

In this lesson, we've expanded on class templates, particularly focusing on how to separate their declarations from definitions. The key takeaways include:

  • Much like regular classes, class templates can be split into declarations and definitions
  • However, separating the declaration and definition of a class template into separate files can lead to linker errors due to the way templates are instantiated at compile time.
  • A common solution to this problem is to provide the template definitions within the header file directly, or through an included .ipp or .tpp file to ensure the compiler has access to them when needed.
  • An alternative, less commonly used method involves explicitly instantiating templates in a source file for each required type combination, which helps in cases where the template types are known in advance, and limited in quantity.

Was this lesson useful?

Next Lesson

Compile-Time Evaluation

Learn how to implement functionality at compile-time using constexpr and consteval
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Templates and Header Files

Learn how to separate class templates into declarations and definitions while avoiding common linker errors

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Templates
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Compile-Time Evaluation

Learn how to implement functionality at compile-time using constexpr and consteval
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved