C++ Coding Style – Preferred Method for Large Template Implementations

ccoding-styleobject-orientedtemplates

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.

Best Answer

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.

Related Topic