3

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)

jhnlmn
  • 381
  • 3
  • 11

2 Answers2

2

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.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • But tuple is not mutable, how would I implement "items.m_itemA.m_value = 123"? I may have dozens or hundreds of named items and thousands of such mutators, which should access items by descriptive names (not indexes) and be efficient. – jhnlmn Jun 24 '21 at 02:27
  • @jhnlmn You already have a thing that collects mutable variables of different types with descriptive names. It is a struct. It's called a struct. – Zan Lynx Jun 24 '21 at 04:23
  • Who said that a tuple is not mutable? That's news to me. The individual members of a tuple can be modified at any time. It was not mentioned in the question that there could be "hundreds" of those things, and a specific need to access them in a certain way. One cannot expect to get an answer to a question that wasn't originally asked. If that was mentioned as a part of the original question, I can think of a slightly alternative answer that I would've given, but I already spent a bit of time, writing and testing my answer. – Sam Varshavchik Jun 24 '21 at 10:51
  • @Sam Varshavchik I appreciate your effort and I am learning new tricks from it, thank you. I apologize about "tuple is not mutable" comment, it is irrelevant in this case. I just asked for clarification on what to replace "items.m_itemA.m_value = 123" with. I figured it myself now: std::get<0>(items.m_itemsImpl).m_value = 123; Right? Alas, this will not work for me since I must have descriptive names for items, like m_itemA or m_itemB or m_myBestItem. As I wrote in initial post, I am planning to have many items. Thank you again. I am sure your answer will be useful for others. – jhnlmn Jun 24 '21 at 20:50
  • 1
    "Hundreds of named items" sounds like there's something fundamentally wrong with this design. Even the most complicated C++ library is not going to have classes with "hundreds of named items and thousands of such mutators". This design should be re-thought completely. Instead of "hundreds of named items", a more appropriate design would be a single associated container, like `map` or `unordered_map` with the name of each item as a key. And there are existing basic techniques for providing named wrappers around calls to `std::get`, too. – Sam Varshavchik Jun 24 '21 at 23:25
  • @Sam Varshavchik This struct is a collection of performance counters for the entire program. Most functions in the program modify their respective counters and a central publsiher serializes them periodically. Those mutators must be as efficient as possible: items.m_itemA.m_value++ or std::get<0>(items.m_itemsImpl).m_value++ is just a single instruction on Intel. But maps or any other fancy containers are unacceptable. As for named wrappers - it may be interesting as long as there is no duplication - that is I have to add or remove an item from a single place, not 2 or more places. – jhnlmn Jun 25 '21 at 00:53
  • Instead of `typedef ItemsImpl Items`, make it a subclass, `struct Items: ItemsImpl { ... }`, with its own `constexpr` constructor. And it can define its own accessor method just once: `auto &mitemsA() { return std::get<0>(m_itemsImpl); }`. Then it's just a matter of `int n=counters.mitemsA();` or `counters.mitemsA()=4`. Adding a new counter involves adding an additional template parameter and writing one accessor. – Sam Varshavchik Jun 25 '21 at 01:37
  • And, of course, not everything has to be done entirely in C++. Define the counters and their types in an XML file and include as part of the build script or Makefile an XSL stylesheet that robo-generates C++ code for the entire class and all accessors. – Sam Varshavchik Jun 25 '21 at 01:38
  • @Sam Varshavchik So, if I remove or insert an item, then I will have to rewrite indexes for all others after it? And also modify ItemsImpl template params? No, sorry, this is worse than the original. I still have a nagging feeling that this is doable in C++. – jhnlmn Jun 25 '21 at 02:06
  • No, you don't. Just like there's a way to remove a value from the middle of the vector without shifting all the values in the vector, as long as the relative order of the values in the vector does not matter. This is all I'm going to say about this subject matter, there's nothing else left to say, except that, yes, this is doable in C++. And you completely ignored another solution that I mentioned, the one that involves more than just C++. – Sam Varshavchik Jun 25 '21 at 02:19
1

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 );

    }
};
jhnlmn
  • 381
  • 3
  • 11