This post is meant to be a complementary answer to existing answers that don't address a very common problem (especially among beginners): including header files with templates implemented in separate source files.
Basically I have multiple .cpp files with all my functions I made for use with binary trees, BST, linked lists, etc.
Since:
- You have tagged your question with c++, and
- C++ has templates, and
- You have container-like data structures (BST, linked list, etc.) which I assume use templates
I'd like to point out to a common problem not mentionned in the other answers. Consider the following two files:
File class.hpp
:
#pragma once // Non-standard but supported by almost (if not) all compilers
// Some class
template<class T> class Class {
T data;
public:
Class();
};
// Some function
template<class T> T square(const T& x);
File class.cpp
:
#include "class.hpp"
#include <iostream>
template<class T> Class<T>::Class()
{
std::cout << "Class()" << '\n';
}
template<class T> T square(const T& x)
{
return x*x;
}
Try and instanciate a Class
object, or call square()
:
#include "class.hpp"
#include <iostream>
int main()
{
Class<int> clss; // should output Class()
std::cout << square<int>(3); // should print 9
}
Compile with:
g++ class.cpp main.cpp -o cpptest.exe
./gcc/bin/ld.exe: main.cpp(.text+0x15): undefined reference to 'Class<int>::Class()'
./gcc/bin/ld.exe: main.cpp(.text+0x28): undefined reference to 'int square<int>(int const&)'
The problem
1. Templates
Imagine you have this function:
template<class T> T add(const T& x, const T& y)
{
return x + y;
}
And you call it like:
int res = add<int>(2, 3);
It's at the moment of the call (i.e. when the compiler sees the call to add()
) when a copy of the function implementation is created by the compiler:
int add(const int& x, const int& y)
{
return x + y;
}
And then your call will become:
int res = add(2, 3);
This obviously implies that the template function implementation must be visible to the compiler when the call is found, so the compiler will be able to make a copy of it. And for the compiler to find/see it, it must be in the same translation unit.
That's how templates magic works under the hood. See this answer for more information.
2. The compiler and the linker
Going through the compilation steps:
- Replace all the
#include
directives with the included file contents.
- Compile every
.cpp
file (also called a "translation unit") separately. The result will be an object file for each unit. Note that some code (like in main.cpp
) refers to another code (class.cpp
). This will be resolved in the next step.
- Link the translation units altogether into an executable file. (See JMAA's answer for more details).
Applying step 1 above, we will have two translation units (.cpp
files):
File class.cpp
:
template<class T> class Class {
T data;
public:
Class();
};
template<class T> T square(const T& x);
/* <iostream> code ... */
template<class T> Class<T>::Class()
{
std::cout << "Class()" << '\n';
}
template<class T> T square(const T& x)
{
return x*x;
}
File main.cpp
:
template<class T> class Class {
T data;
public:
Class();
};
template<class T> T square(const T& x);
/* <iostream> code ... */
int main()
{
Class<int> clss;
std::cout << square<int>(3);
}
Applying step 2 above, we will have just addresses (this is not how real object code looks like, it's just a simplification):
File class.cpp
:
/* Nothing to generate for 'Class' definition. Remember, templates are just models (hence the word "template"), not concrete classes/functions. */
/* Nothing to generate for square()'s definition: It's a template, not concrete. */
iostream_object_code
/* Nothing to generate for 'Class' implementation. It's a template, not concrete. */
/* Nothing to generate for square()'s implementation: It's a template, not concrete. */
File main.cpp
:
/* Class is not concrete: nothing is generated. */
/* square() is not concrete: nothing is generated. */
iostream_object_code
int main()
{
class_int_obj_placeholder clss;
__operator_xx(cout_obj_ref, fn100017(3));
}
Apply step 3 above, and you'll get the undefined reference
errors: The linker doesn't know of any fn100017()
in any translation unit. Because square()
is a template (or a model), the compiler did not generate any concrete implementation. The same applies for Class
.
The solution
- The obvious fix is to put both the definition and the implementation in the same header file. This way, when included in
main.cpp
, both will be visible to the compiler, because they are in the same translation unit.
- Or, if you want to keep your definitions and implementations separate, make your implementations in a header file, then include it at the end of the definition header:
File class.hpp
:
#pragma once
template<class T> class Class {
T data;
public:
Class();
};
template<class T> T square(const T& x);
#include "class_impl.hpp"
File class_impl.hpp
:
#include <iostream>
template<class T> Class<T>::Class()
{
std::cout << "Class()" << '\n';
}
template<class T> T square(const T& x)
{
return x*x;
}
Compiling and running main.cpp
with the same command:
Class()
9