When writing a templated C++ class, you usually have three options:
(1) Put declaration and definition in the header.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
or
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Pro:
- Very convenient usage (just include the header).
Con:
- Interface and method implementation are mixed. This is "just" a readability problem. Some find this unmaintainable, because it is different from the usual .h/.cpp approach. However, be aware that this is no problem in other languages, for example, C# and Java.
- High rebuild impact: If you declare a new class with
Foo
as member, you need to include foo.h
. This means that changing the implementation of Foo::f
propagates through both header and source files.
Lets take a closer look at the rebuild impact:
For non-templated C++ classes, you put declarations in .h and method definitions in .cpp. This way, when the implementation of a method is changed, only one .cpp needs to be recompiled. This is different for template classes if the .h contains all you code. Take a look at the following example:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
Here, the only usage of Foo::f
is inside bar.cpp
. However, if you change the implementation of Foo::f
, both bar.cpp
and qux.cpp
need to be recompiled. The implementation of Foo::f
lives in both files, even though no part of Qux
directly uses anything of Foo::f
. For large projects, this can soon become a problem.
(2) Put declaration in .h and definition in .tpp and include it in .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
#include "foo.tpp"
// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
...
}
Pro:
- Very convenient usage (just include the header).
- Interface and method definitions are separated.
Con:
- High rebuild impact (same as (1)).
This solution separates declaration and method definition in two separate files, just like .h/.cpp. However, this approach has the same rebuild problem as (1), because the header directly includes the method definitions.
(3) Put declaration in .h and definition in .tpp, but dont include .tpp in .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Pro:
- Reduces rebuild impact just as the .h/.cpp separation.
- Interface and method definitions are separated.
Con:
- Inconvenient usage: When adding a
Foo
member to a class Bar
, you need to include foo.h
in the header. If you call Foo::f
in a .cpp, you also have to include foo.tpp
there.
This approach reduces the rebuild impact, since only .cpp files that really use Foo::f
need to be recompiled. However, this comes at a price: All those files need to include foo.tpp
. Take the example from above and use the new approach:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
As you can see, the only difference is the additional include of foo.tpp
in bar.cpp
. This is inconvenient and adding a second include for a class depending on whether you call methods on it seems very ugly. However, you reduce the rebuild impact: Only bar.cpp
needs to be recompiled if you change the implementation of Foo::f
. The file qux.cpp
needs no recompilation.
Summary:
If you implement a library, you usually do not need to care about rebuild impact. Users of your library grab a release and use it and the library implementation does not change in the user's day to day work. In such cases, the library can use approach (1) or (2) and it is just a matter of taste which one you choose.
However, if you are working on an application, or if you are working on an internal library of your company, the code changes frequently. So you have to care about rebuild impact. Choosing approach (3) can be a good option if you get your developers to accept the additional include.