jhnlmn
jhnlmn

Reputation: 401

How to create a collection of member variables at compile time in C++?

Currently I have this:

#include <iostream>
#include <array>

struct ItemBase {
    virtual void print() const = 0;
};
template <typename T> struct Item : ItemBase {
    T m_value {};
    void print() const override { 
        std::cout << m_value << std::endl;
    }
};
struct Items {
    constexpr Items() {}//constexpr helps in ensuring constant initialization

    Item<int>    m_itemA;
    Item<double> m_itemB;
    // Many more items will be added and deleted often
    
    //ItemBase * m_items[2] {
    //Or
    std::array<ItemBase *, 2> m_items {
        &m_itemA,
        &m_itemB,
    };
    void print() {
        for(ItemBase * item : m_items) {
            item->print();
        }
    }
};
Items items; //global scope. Should be placed directly to .data without any runtime constructor
int main(int argc, char ** argv) {
    items.m_itemA.m_value = 123; //Some sample use
    items.m_itemB.m_value = 1.23;
    items.print();
    return 0;
}

It is very tedious and error prone to always modify m_items array size and initializer every time I need to add/remove m_itemX. How can I trick C++ compiler to do that for me?

An obvious fix would be to use macros, like this:

#define ITEMS(WRAPPER) \
    WRAPPER(Item<int>, m_itemA) \
    WRAPPER(Item<double>, m_itemB)
#define WRAPPER_INSTANCE(type, name) type name;
#define WRAPPER_POINTER(type, name) &name,
#define WRAPPER_0(type, name) 0,

struct Items {
    constexpr Items() {}//constexpr helps in ensuring constant initialization

    ITEMS(WRAPPER_INSTANCE);
        
    std::array<ItemBase *, std::size( { ITEMS(WRAPPER_0) } ) > m_items {
        ITEMS(WRAPPER_POINTER)
    };
    ...

This works, but macros are so ugly, IDE code browsing is confused.

Any better solution?

It would be OK to add some parameters or a wrapper to m_itemX instantiations. But no runtime constructor and certainly no allocations are allowed - otherwise I could have used vector or a list.

(depending on implementation, alignment and padding it may be possible to add m_size variable to ItemBase and have print() simply scan body of Items instead of keeping m_items array)

Upvotes: 3

Views: 1300

Answers (2)

jhnlmn
jhnlmn

Reputation: 401

I found one approach: assemble item pointers in a constexpr container at compile time. Usage looks like this:

struct Items {
    ...
    static inline Item<int>    m_itemA;
    container_add(&m_itemA);
    static inline Item<double> m_itemB;
    container_add(&m_itemB);
    ...
};

I used idea of template specialization associated with source __LINE__, which references previous template specialization from Does C++ support compile-time counters?

First we declare a recursive specialization of ItemContainer, which copies items container from the previous line:

template<unsigned int Line> struct ItemContainer 
{ static constexpr inline auto items { ItemContainer<Line-1>::items }; };

Then specialization for line 0 with empty container:

template<> struct ItemContainer<0> { static constexpr inline std::tuple<> items {}; };

Then explicit specialization for given __LINE__ with a new item to be appended:

#define container_add(newItem) template<> struct ItemContainer<__LINE__> {\
    static inline constexpr auto items { \
    std::tuple_cat ( ItemContainer<__LINE__-1>::items, std::tuple<ItemBase*> (newItem) ) }; }

Then accessor for the accumulated container:

#define container_last ItemContainer<__LINE__>

And print:

void print() {
    std::apply([](auto&&... args) { ( ( 
        args? (void) args->print() : (void) (std::cout << (void*)args << std::endl)
            ), ...);}, container_last::items );
}

One limitation is that it requires C++17.

Another major limitation is that it can work only for static items - either global variables or static members of a class. This is OK for my purpose since my items was a global variable anyway.

Also, currently this code does not compile with g++ due to a bug: Bug 85282 - CWG 727 (full specialization in non-namespace scope) This is a show stopper for now, hopefully they will fix it eventually. For now it can be compiled with clang++ 10.0.0 and cl 19.16.27024.1 from VS 2017. This bug only happens when adding members of a class. g++ still allows us to add global variables outside class scope.

Also, I hoped to eliminate any item name duplication, but here I still have to type every item name twice - on adjacent lines, but this much better than having to enter names in completely different places, which was very error prone.

Final challenge is to choose the right kind of item container. I tried std::tuple, std::array, custom list and recursive typename references. I showed tuple version above - it is the shortest, but the slowest to compile and supports the fewest number of items. The custom list is the fastest and allows the largest item count (1000 and even 10,000 with cl). All those versions generate very efficient code - the container itself is not present in RAM at all, print() function is compiled into a sequence of calls to individual item->print() where item addresses are constant.

Here is full implementation using custom list:

struct ListItem {
    ItemBase       * m_item;
    const ListItem * m_prev;
    int              m_count;
};
template <int N>
constexpr std::array<ItemBase*,N> reverse_list(const ListItem * last) {
    std::array<ItemBase*,N> result {};
    for(int pos = N-1; pos >= 0 && last && last->m_item; pos--, last = last->m_prev) {
        result[pos] = last->m_item;
    }
    return result;
}

struct Items {
    constexpr Items() {}//constexpr helps in ensuring constant initialization

    /*
    Idea of template specialization, which references previous template specialization is from:
    https://stackoverflow.com/a/6210155/894324

    There is gcc bug "CWG 727 (full specialization in non-namespace scope)": 
    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85282
    Explicit specialization in class scope should have been allowed in C++17, 
    but g++ 9.3.0 in 2021 still says "error: explicit specialization in non-namespace scope"
    So, until g++ is fixed we should use clang++ or MS cl
    */

    template<unsigned int Line> struct ItemContainer 
        { static constexpr inline ListItem m_list { ItemContainer<Line-1>::m_list }; };
    template<> struct ItemContainer<0> { static constexpr inline ListItem m_list {nullptr, nullptr, 0}; };
    #define container_last ItemContainer<__LINE__>
    #define container_add(newItem) template<> struct ItemContainer<__LINE__> {  \
        static constexpr inline ListItem m_list { \
            newItem, \
            &ItemContainer<__LINE__-1>::m_list, \
            ItemContainer<__LINE__-1>::m_list.m_count + 1 \
        };     }
    static inline Item<int>    m_itemA;
    container_add(&m_itemA);
    
    //.... Thousands of extra items can be added ....
    
    static inline Item<long long>    m_itemB;
    container_add(&m_itemB);
    

    void print() {
        std::cout << "list (last to first):" << std::endl;
        for(const ListItem * item = &container_last::m_list; item && item->m_item; item = item->m_prev) {
            item->m_item->print();
        }
        std::cout << "reversed:" << std::endl;
        const auto reversed_list = reverse_list<container_last::m_list.m_count>(&container_last::m_list);
        std::apply([](auto&&... args) { ( ( 
            args? (void) args->print() : (void) (std::cout << (void*)args << std::endl)
             ), ...);}, reversed_list );

    }
};

Upvotes: 1

Sam Varshavchik
Sam Varshavchik

Reputation: 118425

This should be doable by using variadic templates, and std::tuple. I'll sketch out a rough blueprint for how to declare it, which should make it clear how the complete implementation will look like.

#include <iostream>
#include <array>
#include <utility>
#include <tuple>

struct ItemBase {
    virtual void print() const = 0;
};

template <typename T> struct Item : ItemBase {
    T m_value {};
    void print() const override {
        std::cout << m_value << std::endl;
    }
};

template<typename tuple_type, typename index_sequence> struct ItemsBase;

template<typename ...Type, size_t ...n>
struct ItemsBase<std::tuple<Type...>,
                 std::integer_sequence<size_t, n...>> {

    constexpr ItemsBase() {}

    std::tuple<Item<Type>...> m_itemsImpl;

    std::array<ItemBase *, sizeof...(Type)> m_items{
        &std::get<n>(m_itemsImpl)...
    };

    void print()
    {
           // ... see below
    }
};

template<typename ...Types>
using ItemsImpl=ItemsBase<std::tuple<Types...>,
              std::make_index_sequence<sizeof...(Types)>>;

typedef ItemsImpl<int, double> Items;

Your existing Items class is just an alias for ItemsImpl<int, doule>. Whenever you want to add another field, just add a template parameter.

All instances of the items are collected into a single std::tuple, and m_items gets initialized accordingly.

Implementing the various methods depends on your C++ version. With C++17 it's just a simple fold expression:

void print() {
    ( std::get<n>(m_itemsImpl).print(), ...);
}

With C++11 or C++14 this is still doable, but will require more work using (probably) some helper classes.

Upvotes: 2

Related Questions