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.
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.
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:
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"
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.
In this lesson, we've expanded on class templates, particularly focusing on how to separate their declarations from definitions. The key takeaways include:
.ipp
or .tpp
file to ensure the compiler has access to them when needed.Learn how to separate class templates into declarations and definitions while avoiding common linker errors
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.