Home > Blockchain >  Extract parts of a template into a cpp file
Extract parts of a template into a cpp file

Time:06-01

When you are building a class template, often the whole of the template needs to be in a header file, which is bad because heavily templated code is hard to reason about, takes long to compile and generates subpar error messages. But often times, parts of the template class that are directly related to the template-arguments can be isolated into a couple small functions and the rest of the code can be regular code that makes calls to these functions when needed:

#include <cstddef>

template <class T>
struct Array {
    std::byte* data;
    std::size_t size;
    std::size_t capacity;

    void construct_at(void* p); // 
    void destruct_at(void* p);  // Directly type-related
    size_t obj_size();          // 


    void push_back() {                              // 
        if (size == capacity) {                     //
            // ... resize ...                       //
        }                                           //
        construct_at(data   size * obj_size());     //
          size;                                     // Not directly
    }                                               // type related
                                                    //
    void pop_back() {                               //
        auto const off = (size - 1) * obj_size();   //
        destruct_at(data   off);                    //
        --size;                                     //
    }                                               //
};

Is there a way to extract parts of this class that are not directly related to the type (push_back, pop_back) into non-templated code that happens to make calls to templated functions (construct_at, destruct_at, obj_size) that somehow do the right-thing for the type at hand?

In theory, this can be done with virtual functions:

#include <cstddef>

struct ArrayBase {
    std::byte* data;
    std::size_t size;
    std::size_t capacity;

    virtual void construct_at(void* p) = 0; // 
    virtual void destruct_at(void* p) = 0;  // pure-virtual
    virtual size_t obj_size() = 0;          // 


    void push_back(); //
    void pop_back();  // defined in a cpp file
};


template <class T>
struct Array : ArrayBase {
    void construct_at(void* p) override final {
        new (p) T;
    }

    void destruct_at(void* p) override final {
        static_cast<T*>(p)->~T();
    }

    size_t obj_size() override final {
        return sizeof(T);
    }
};

The main issue here is that we need to go through a virtual-table. But in our code, there is no runtime dynamism, it should be possible to avoid that indirection. Is that the case?

CodePudding user response:

One solution, that is not very general but is applicable sometimes is to create a non-type-safe version of the class that directly works with bytes, and then create a type-safe version that wraps around the complicated logic:

#include <cstddef>

struct ArrayBase {
private:
    std::byte* data;
    std::size_t size_in_byes;
    std::size_t capacity_in_bytes;

protected:

    void* push_back(size_t bytes);  // Complicated logic is
    void* pop_back(size_t bytes);   // moved to a .cpp file
};

template <class T>
struct Array : private ArrayBase {
    void push_back() {
        auto p = push_back(sizeof(T));
        new (p) T;
    }

    void pop_back() {
        auto p = pop_back(sizeof(T));
        static_cast<T*>(p)->~T();
    }
};

This is viable if the non-type-safe version is significantly more complex than the type-safe version.

CodePudding user response:

If I understand you right then you wonder if ArrayBase::push_back() can avoid going through the virtual table to call Array::construct_at(). But if the goal is to reduce the size of the header and improve compile times then that is a clear NO, unless you use LTO (link time optimization).

If you have push_back() in it's own compilation unit and compile it only once then there is no way for the compiler to know what version of construct_at is the right one to call when compiling push_back(). There is also no way for the compiler to inline the call to push_back() when it is used and it knows the type T.

The exception to this is when you use LTO, since then the whole source basically turns into a single compilation unit at link time and all sorts of whole program optimizations can happen, including deducting and eliminating virtual function calls into static calls (or inlineing said calls).

Similar if you leave the whole code in the header. But then what was the point if splitting out the non-template parts?

CodePudding user response:

An easier way to extract is when methods are really not related to template.

You might try to remove dependencies to type in several ways.

virtual as you have done, or try function pointers, something like:

#include <cstddef>

struct ArrayBase {
    std::byte* data;
    std::size_t size;
    std::size_t capacity;

#define OPTION moreReadableOption
#if OPTION == 1
    void push_back(void (*construct_at)(void*));
    void pop_back(void (*destruct_at)(void*));
#elif OPTION == 2
    void push_back(std::type_identity_t<void(void*)>* construct_at);
    void pop_back(std::type_identity_t<void(void*)>* destruct_at);
else
    using construct_at_func = void(void*);
    using destruct_at_func = void(void*);

    void push_back(construct_at_func* construct_at);
    void pop_back(destruct_at_func* destruct_at);
#endif

};

template <class T>
struct Array : ArrayBase {
    static void construct_at(void* p) { new (p) T; }
    static void destruct_at(void* p) { static_cast<T*>(p)->~T(); }
    static size_t obj_size() { return sizeof(T); }

    void push_back() { ArrayBase::push_back(&construct_at); }
    void pop_back() { ArrayBase::pop_back(&destruct_at); }
};

cpp files:

void ArrayBase::push_back(void (*construct_at)(void*))
{
    if (size == capacity) {
        // ... resize ...
    }
    construct_at(data   size * obj_size());
      size;
}

void ArrayBase::pop_back(void (*destruct_at)(void*)) {
    auto const off = (size - 1) * obj_size();
    destruct_at(data   off);
    --size;
}

CodePudding user response:

Something like

Header:

#include <cstddef>
#include <memory>

struct TypeConfBase {
    virtual void construct_at(void* p) = 0; // 
    virtual void destruct_at(void* p) = 0;  // pure-virtual
    virtual size_t obj_size() = 0;          // 
};

template <class T>
struct TypeConf : TypeConfBase {
    void construct_at(void* p) override final {
        new (p) T;
    }

    void destruct_at(void* p) override final {
        static_cast<T*>(p)->~T();
    }

    size_t obj_size() override final {
        return sizeof(T);
    }
};

CPP File (struct definition of course has to be in header to use it):

struct ArrayBase {
    std::unique_ptr<TypeConfBase> typeconf;

    std::byte* data;
    std::size_t size;
    std::size_t capacity;

    void push_back(); //
    void pop_back();  // defined in a cpp file
};
  • Related