Skip to main content

Split C++ Template Declaration and Implementation

Description

Splitting the template declaration and implementation is not easy in C++ because the actual templates code is generated at compile time. However, it is still possible to split them out if you do know the types that you would like to generate as the role of a library.

Core Idea

The major ideas behind splitting the declaration and implementation is to convert a compile-time error into a link-time error. If the library user is calling anything that the library provider does not allow, the compile time is actually OK since we do provide a simple declaration about it. However, when linking into the final executable with the library provided, we'll find that there is no definition of the very symbol that the user called.

main.cpp:(.text+0x01): undefined reference to `LovelyClass<EvilUser>::LovelyFunction()'

 

Role Compile Time Link Time
Library Provider
Library User

Template Instantiation

Template, in C++, is actually a hint (and then further a type) for the compiler. There's no corresponding code generated if there's no use of its definition, which therefore come up with the first solution. Expose any type to which you would like your library user to have access can help split up the declaration and implementation.

//! file: lib.h
template <typename T> struct Foo { T f(); }
// template struct Foo<float>;  // Create a comment so that the poor user know

//! file: lib.cpp
template <typename T> T Foo<T>::f() { return 42; }
template struct Foo<float>; // <----- Template Instantiation for type float

//! file: main.cpp
Foo<float>().f();  // Compile: √, Link: √
// Foo<int>().f(); // Compile: √, Link: ✕

When we write the following line, we are asking the compiler to include a generated version of struct Foo specialized for float type. The process is named Template Instantiation.

Pimpl: Pointer to Implementation

Pimpl is a famous pattern in C++. (Well ... patterns do not live only in Java.) Pimpl is an abbreviation for Pointer to Implementation.

The idea is very simple. In C++, we cannot define the type if the compiler does not know how big the type is (or how many bytes it takes) because we need to know how much stack it'll take when we enter a function (maybe main()). However, the size of a pointer is determined according to the architecture when compiled, which therefore have enabled us to put a pointer to an unexposed type as a substitute for whatever we would like to hide.

Let's take the following structure as an example:

template <typename T> struct Foo { int i; float f; T t; };

We can hide the whole struct information by using a pointer to struct Foo as struct FooImpl.

//! file: lib.h
template <typename T> struct FooImpl;
template <typename T> struct Foo { std::shared_ptr<FooImpl<T>> pimpl; }
//! file: lib.cpp
template <typename T> struct FooImpl { int i; float f; T t; };
template struct FooImpl<float>;  // Do instantiation so that Foo<float> can link

Sum up

You can have both of the techniques used as a combo in your library.

//! file: lib.h
#include <memory>

template <typename T> struct FooImpl;
template <typename T> struct Foo {
  Foo();
  T f();

  std::shared_ptr<FooImpl<T>> pimpl;
};
// template struct Foo<float>;    // Create a comment so that the poor user know
// template struct Foo<double>;

// ==============================

template <typename T, T _> T bar();

// template int bar<int, 1>();
// template int bar<int, 2>();
// template int bar<int, ...>();
// template int bar<int, 100>();
//! file: lib.cpp
#include "lib.h"

template <typename T> struct FooImpl {
  FooImpl() = default;
  int i; float f; T t;
};

template <typename T> Foo<T>::Foo() {
  pimpl = std::make_shared<FooImpl<T>>();
  pimpl->i = 42;
  pimpl->f = 3.14;
}

template <typename T> T Foo<T>::f() { return this->pimpl->t; }
template <> float Foo<float>::f() { return this->pimpl->f; }
template <> double Foo<double>::f() { return this->pimpl->f * 2; }

template struct Foo<float>;
template struct Foo<double>;

// ==============================

template <typename T, T num> T bar() { return num + bar<T, num - 1>(); }
template <> int bar<int, 1>() { return 1; }
template int bar<int, 100>();   // Do instantiation in the range of 1..100
//! file: main.cpp
#include "lib.h"
#include <stdio.h>

int main() {
  printf("%.2f\n", Foo<float>().f());
  printf("%.2lf\n", Foo<double>().f());

  // Error: no int type generated
  // printf("%.2d\n", Foo<int>().f());

  printf("%d\n", bar<int, 100>());
  printf("%d\n", bar<int, 50>());
  printf("%d\n", bar<int, 1>());

  // Error: no 101 generated
  // printf("%d\n", bar<int, 101>());

  return 0;
}

 

Comments

Comments powered by Disqus