16

Typically when declaring a C++ class, it is best practice to put only the declaration in the header file and put the implementation in a source file. However, it seems that this design model does not work for template classes.

When looking online there seems to be 2 opinions on the best way to manage template classes:

1. Entire declaration and implementation in header.

This is fairly straightforward but leads to what are, in my opinion, difficult to maintain and edit code files when the template becomes large.

2. Write the implementation in a template include file (.tpp) included at end.

This seems like a better solution to me but doesn't seem to be widely applied. Is there a reason that this approach is inferior?

I know that many times the style of code is dictated by personal preference or legacy style. I am starting a new project (porting an old C project to C++) and I am relatively new to OO design and would like to follow best practices from the start.

4

4 Answers 4

13

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.

2
  • I've been an adapt of your #3 for several years. Except my files are .h (for declarations), .hpp (for templated code definitions), and often .cpp (for non-template code definitions, and possibly pre-instantiations of some templates with specific types).
    – ddevienne
    Commented Mar 9, 2021 at 12:23
  • What you omit to mention in #3 for the PROs is that fact the .tcc file (or .hpp in my case) typically will have more #includes than the .h with only declarations, thus it's not only reduced build times from direct-changes to .tcc/.hpp files, but also from indirect changes avoided by having fewer includes in the .h files. In large code bases, this matters a lot.
    – ddevienne
    Commented Mar 9, 2021 at 12:27
2

Similar to the .tpp idea (which I have never seen used), we put most inline functionality into a -inl.hpp file which is included at the end of the usual .hpp file.

As other's indicate, this keeps the interface readable by moving the clutter of the inline implementations (like templates) in another file. We allow some interface inlines but try to limit them to small, typically single line functions.

1

One pro coin of the 2nd variant is that your headers look more tidy.

The con might be you may have the inline IDE error checking, and debugger bindings screwed up.

1
  • 2nd also requires a lot of template parameter declaration redundancy, which can become very verbose especially when using sfinae. And contrary to the OP I find the 2nd harder to read the more code there is, specifically because of the redundant boilerplate.
    – Sopel
    Commented Jul 11, 2018 at 22:11
0

I greatly prefer the approach of putting the implementation in a separate file, and having just the documentation and declarations in the header file.

Perhaps the reason you haven't seen this approach used in practice much, is you haven't looked in the right places ;-)

Or - perhaps its because it takes a little extra effort in developing the software. But for a class library, that effort is WELL worth while, IMHO, and pays for itself in a much easier to use/read library.

Take this library for example: https://github.com/SophistSolutions/Stroika/

The entire library is written with this approach and if you look through the code, you will see just how well it works.

The header files are about as long as the implementation files, but they are filled with nothing but declarations and documentation.

Compare Stroika's readability with that of your favorite std c++ implementation (gcc or libc++ or msvc). Those all use the inline in-header implementation approach, and though they are extremely well written, IMHO, not as readable implementations.

Not the answer you're looking for? Browse other questions tagged or ask your own question.