C++ templates – Beginners most common issue

This article relates to the meta-programming series, but a bit external, and I consider it as a bonus article for beginners. We are going to talk about separation of templates declarations/definitions across files.

Previous article in series: Basic templates usage – Part 2
Next article in series: C++ – Partial / Explicit specialization

The Problem

We all started with a single-page application, when there are a declaration & a definition of a template function/class somewhere in the same main.cpp file. Everything seemed to work and it looked something like that:

#include <iostream>

template <typename T>
T add(T n1, T n2);

int main() {
    std::cout << "Int add: " << add(1, 3) << std::endl;
    std::cout << "Float add: " << add(2.5, 1.4) << std::endl;
    return EXIT_SUCCESS;
}

template <typename T>
T add(T n1, T n2) {
    return n1 + n2;
}

One day, you learn that in order to maintain an application, you should separate functions/classes to different files of .h for declarations and .cpp for definitions and use includes to use functionality across files.

// operations.h

namespace operations { 
    template <typename T> T add(T n1, T n2);

    template <typename T> T sub(T n1, T n2);

    template <typename T> T div(T n1, T n2);

    template <typename T> T mul(T n1, T n2);
}
// operations.cpp

#include "operations.h"

namespace operations {
    template <typename T>
    T add(T n1, T n2) {
        return n1 + n2;
    }

    template <typename T>
    T sub(T n1, T n2) {
        return n1 - n2;
    }

    template <typename T>
    T mul(T n1, T n2) {
        return n1 * n2;
    }

    template <typename T>
    T div(T n1, T n2) {
        return n1 / n2;
    }
}
// main.cpp

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

int main() {
    std::cout << "Int add: " << operations::add(1, 3) << std::endl;
    std::cout << "Float add: " << operations::add(2.5, 1.4) << std::endl;
    return EXIT_SUCCESS;
}

But then, this happened:

path/main.cpp.o: In function `main':
path/main.cpp:line: undefined reference to `int operations::add<int>(int, int)'
path/main.cpp:line+1: undefined reference to `double operations::add<double>(double, double)'

At this stage, you might say “I prefer a working unmaintainable application over a non-working maintainable application”, and you would be right in general, however it’s not the case here.

Why is this happening?

Before we’ll go through optional solutions and their pros & cons, let’s first understand why is this happening.

Rule: To create an specialized class in the instantiation process to compiler have three requirements:

  1. A template function signature (declaration).
  2. A template function definition.
  3. The type/s to instantiate this template function (“pattern”) with.

Let’s go through the linker/instantiation process:

// main.cpp

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

int main() {
    std::cout << "Int add: " << operations::add(1, 3) << std::endl;
    std::cout << "Float add: " << operations::add(2.5, 1.4) << std::endl;
    return EXIT_SUCCESS;
}

The compiler now able to see the function declarations.

int main() {
    std::cout << "Int add: " << operations::add(1, 3) << std::endl;
    std::cout << "Float add: " << operations::add(2.5, 1.4) << std::endl;
    return EXIT_SUCCESS;
}

The compiler deduces the type int, and goes to operations::add function with [T = int]. When it get there, it can’t find the definition for this function, and 2/3 requirements are not enough to complete the instantiation, so you get the following situation in the linker process:

template <typename T> T operations::add(T n1, T n2);

template <typename T> T operations::sub(T n1, T n2);

template <typename T> T operations::div(T n1, T n2);

template <typename T> T operations::mul(T n1, T n2);

template <> int operations::add<int>(int n1, int n2); // incomplete specialization

template <typename T>
T operations::add(T n1, T n2) {
    return n1 + n2;
}

template <typename T>
T operations::sub(T n1, T n2) {
    return n1 - n2;
}

template <typename T>
T operations::mul(T n1, T n2) {
    return n1 * n2;
}

template <typename T>
T operations::div(T n1, T n2) {
    return n1 / n2;
}

int main() {
    std::cout << "Int add: " << operations::add(1, 3) << std::endl;
    std::cout << "Float add: " << operations::add(2.5, 1.4) << std::endl;
    return EXIT_SUCCESS;
}

On line 32 we can see that the call will try navigate to the definition of the declaration in line 9, but won’t be able to find one, which will produce the linker error:

undefined reference to `int operations::add<int>(int, int)'

Optional Solutions

To solve this issue we have to assure the connection between the declaration and the definition, before the instantiation process takes place.

Control the Instantiation Place

One way to assure the connection, is to tell the compiler to instantiate the function/class with specific template parameters, inside the definitions page (.cpp). Check it out:

// operations.cpp

#include "operations.h"

namespace operations {
    template <typename T>
    T add(T n1, T n2) {
        return n1 + n2;
    }
    /*...*/
}

template int operations::add(int, int);
template double operations::add(double, double);
// Actually, the above 2 lines use compiler deduction:
// template int operations::add<int>(int, int);
// template double operations::add<double>(double, double);

// Assume we have a template class in header file: template <typename T> class Foo {...}
// We would write here: template class Foo<int>; ...

This way whenever the definitions are being generated, the instantiations will immediately take place, and problem solved.

Pros:

  • Full separation between definitions & declarations- Readability, maintainable code, etc..
  • Full control over allowed instantiations, no one in other parts of code can create a new instantiation.

Cons:

  • When there are a lot of allowed instantiation, or unlimited, you’ll have to specify every one of them. This part can be really unpleasant when talking about multiple template parameters, and the need to specify all of the legal combinations.
  • One more templates syntax to remember, every time you want to deal with templates.

Merge Declarations & Definitions

Probably the easiest way to solve it is to avoid declarations & definitions separation in template classes/functions.

// operations.h

#ifndef TESTS_OPERATIONS_H
#define TESTS_OPERATIONS_H

namespace operations {
    template <typename T>
    T add(T n1, T n2) {
        return n1 + n2;
    }
    /*...*/
}

#endif //TESTS_OPERATIONS_H

This way we can instantiate new specialization from every place that includes the header file in our code.

Pros:

  • No new syntax.
  • Easy to remember.
  • Infinite legal possibilities, without need to explicitly specify them.

Cons:

  • No declaration-definition separation- Harder to maintain the code.
  • Overhead when restrictions over the legal template parameters possibilities are require. Require new syntax, and more templates understanding- sometimes not for beginners.

Include .cpp Inside .h Files

Well. nobody will stop you from fooling yourself, when there actually only a visual declarations-definitions separation. This one looks like this:

// operations.h

#ifndef TESTS_OPERATIONS_H
#define TESTS_OPERATIONS_H

namespace operations {
    template <typename T> T add(T n1, T n2);

    template <typename T> T sub(T n1, T n2);

    template <typename T> T div(T n1, T n2);

    template <typename T> T mul(T n1, T n2);
}

#include "operations.cpp"

#endif //TESTS_OPERATIONS_H
// operations.cpp

//#include "operations.h" // Attention - In a comment!

namespace operations {
    template <typename T>
    T add(T n1, T n2) {
        return n1 + n2;
    }

    template <typename T>
    T sub(T n1, T n2) {
        return n1 - n2;
    }

    template <typename T>
    T mul(T n1, T n2) {
        return n1 * n2;
    }

    template <typename T>
    T div(T n1, T n2) {
        return n1 / n2;
    }
}

Pros:

  • Visual separation of declarations-definitions – Easier to maintain(?).
  • Infinite legal possibilities, without need to explicitly specify them.

Cons:

Including .cpp files is never a good sign, and might significantly increase the compilation time. Assume we have more than just template functions in our .h file, that both template & non-template function definitions are in the same .cpp file. If we include the .cpp file into our .h file, a lot of unnecessary functions will make our code to be re-compiled whenever we change a little thing inside them. It’s harder to maintain a code that every change causes large compilation time.

Instead of taking as-is this solution, it’s better to write the template definitions after the declarations inside the .h file. This way only changes in the template functions will cause a re-compilation for changes inside them. However, it might be easier to just not separate declaration-definition at all in this case.

Conclusion

There will be always pros & cons for every decision, and it’s all about trade-offs. Personally, I usually don’t separate template declarations-definitions, but I do separate template declarations from non-template declarations, this way I keep the code as easy as possible to follow and to understand.

Have a special way of working rules with templates? Found a different way to overcome this issue? Share with us!

More posts in this series:

Leave a comment