Skip to content

Template Implementation Separation

williamfgc edited this page May 4, 2017 · 6 revisions

ADIOS2 classes separate declarations from their implementation. Typically this is done by having a separate .h and .cpp file, however it get's more complicated when templates are involved. Template separation is beneficial for mainly 3 reasons:

  1. Cleaner code interface: separate declaration from implementation (definition).
  2. Reduce compile times: same effects as non-template functions when inlining doesn't produce any benefits (bottleneck is not in calling the template function).
  3. Limit types scope: via explicit template implementation and specialization in compiled object (*.o) files.

To maintain the distinct separation between template definition and implementation in ADIOS2, we use explicit instantiation with 4 different source file types:

  • ClassName.h
    • The main header file containing only the class and member declarations with no implementation. This also contains the declarations for explicitly instantiated members.
  • ClassName.inl
    • A file containing inline function implementations that need to be made public. This is to be included at the bottom of ClassName.h and should only contain implementations that need to be made public.
  • ClassName.tcc
    • A file containing most of the template implementations that can be hidden through explicit instantiation.
  • ClassName.cpp
    • A file containing the non-template implementations and the explicit instation of any template members.

Example

Here is an example of a simple class Foo with template member functions Bar1 and Bar2

Before separation of public and private template implementation

Foo.h containing all implementation
#ifndef FOO_H_
#define FOO_H_

namespace adios
{

class Foo
{
public:
    Foo()
    : m_Bar1Calls(0), m_Bar2Calls(0), m_Bar3Calls(0);
    {
    }

    virtual ~Foo() = default;

    template<typename T>
    void Bar1()
    {
        Bar1Helper<T>();
    }

    template<typename T>
    void Bar2()
    {
        Bar2Helper<T>();
    }

    void Bar3()
    {
        Bar3Helper();
    }

private:
    template<typename T>
    void Bar1Helper()
    {
        ++m_Bar1Calls;
    }

    template<typename T>
    void Bar2Helper()
    {
        ++m_Bar2Calls;
    }

    void Bar3Helper()
    {
        ++m_Bar3Calls;
    }

    size_t m_Bar1Calls;
    size_t m_Bar2Calls;
    size_t m_Bar3Calls;
};

} // end namespace adios
#endif // FOO_H_

After separation of public and private template implementation

In this example, we want to hide the template implementation from the header. We will implement this such that Bar1 is only callable from the core numeric types, i.e. ints, floats, and complex, while Bar2 is callable from all types. This will necessitate that Bar1 and it's helper function is implemented in a .tcc file with explicit instantiation for the allowed types while Bar2 and it's helper function will need to be inlined in the .inl file to be accessible for all types. We will also use a helper macro ADIOS provides to iterate over the core numeric types for the explicit instantiation of Bar1.

Foo.h containing only prototypes and explicit instantiation declarations
#ifndef FOO_H_
#define FOO_H_

#include "ADIOSMacros.h"

namespace adios
{
class Foo
{
public:
    Foo();
    virtual ~Foo() = default;

    template<typename T>
    void Bar1();

    template<typename T>
    void Bar2();

    void Bar3();
private:
    template<typename T>
    void Bar1Helper();

    template<typename T>
    void Bar2Helper();

    void Bar3Helper;

    size_t m_Bar1Calls;
    size_t m_Bar2Calls;
    size_t m_Bar3Calls;
};

// Create declarations for explicit instantiations
#define declare_explicit_instantiation(T)       \
    extern template void Foo::Bar1<T>();

ADIOS_FOREACH_TYPE_1ARG(declare_explicit_instantiation)
#undef(declare_explicit_instantiation)
} // end namespace adios

#include "Foo.inl"
#endif // FOO_H_

Note here that Bar1Helper does not need an explicit instantiation because it's not a visible funtion in the callable interface. It's implementaion will be available to Bar1 inside the tcc file where it's called from.

Foo.inl containing template implementations that always need to be included
#ifndef FOO_INL_
#define FOO_INL_
#ifndef FOO_H_
#error "Inline file should only be included from it's header, never on it's own"
#endif

// No need to include Foo.h since it's where this is include from

namespace adios
{

template<typename T>
void Foo::Bar2()
{
    Bar2Helper<T>();
}

template<typename T>
void Foo::Bar2Helper()
{
    ++m_Bar2Calls;
}

} // end namespace adios

#endif // FOO_INL_
Foo.tcc containing template implementations that should be restricted to only known types
#ifndef FOO_TCC_
#define FOO_TCC_

#include "Foo.h"
namespace adios
{

template<typename T>
void Foo::Bar1()
{
    Bar1Helper<T>();
}

template<typename T>
void Foo::Bar1Helper()
{
    ++m_Bar1Calls;
}

} // end namespace adios

#endif // FOO_TCC_
Foo.cpp containing non-template implementations and explicit instantiations definitions for known types.
#include "Foo.h"
#include "Foo.tcc"

namespace adios
{

Foo::Foo()
: m_Bar1Calls(0), m_Bar2Calls(0), m_Bar3Calls(0)
{
}

void Foo::Bar3()
{
    Bar3Helper();
}

void Foo::Bar3Helper()
{
    ++m_Bar3Calls;
}

// Create explicit instantiations of existing definitions
#define define_explicit_instantiation(T)  \
    template void Foo::Bar1<T>();

ADIOS_FOREACH_TYPE_1ARG(define_explicit_instantiation)
#undef(define_explicit_instantiation)

} // end namespace adios