Search code examples
c++templatesgenerics

What is the approach to handle optional class members?


Processing large amounts of data (gigabytes) I use indexes to data arrays. Since access to data could lead to cache inefficiency, I want to cache some data from array together with the index which gives dramatic speedup for operations through indexes.

The amount of cached data is compile-time choice which should include zero amount of cache data. I have large amount of indexes, so in this case I don’t want to pay for extra “empty” element like std::array does, for example.

So, I made a template with a specialization:

using index_t = unsigned int;
using lexem_t = unsigned int;

template <std::size_t t_arg_cache_line_size>
struct lexem_index_with_cache_t {
    index_t index;
    std::array<lexem_t, t_arg_cache_line_size> cache_line;

    constexpr std::size_t cache_line_size() const {
        return t_arg_cache_line_size;
    }
};

template<>
struct lexem_index_with_cache_t<0> {
    index_t index;
    static std::array<lexem_t, 0> cache_line;

    constexpr std::size_t cache_line_size() const {
        return 0;
    }
};

std::array<lexem_t, 0> lexem_index_with_cache_t<0>::cache_line;

The problem is this “hack” I used in the specialization with zero size which utilizes static member to give formal access to the cache_line while is it empty and the access is not really needed. This allows me to avoid specializations in functions which use this template, like here:

using lexem_index_with_cache = lexem_index_with_cache_t<0>;

template <typename T>
class seq_forward_comparator_cached
{
    const std::vector<T>& vec;
public:
    seq_forward_comparator_cached(const std::vector<T>& vec) : vec(vec) { }

    bool operator() (const lexem_index_with_cache& idx1, const lexem_index_with_cache& idx2)
    {
        if (idx1.index == idx2.index) {
            return false;
        }

        const auto it1_cache_line = idx1.cache_line;  // This code wouldn’t compile in absence of static “hack”
        const auto it2_cache_line = idx2.cache_line;  // This code wouldn’t compile in absence of static “hack”

        auto res = std::lexicographical_compare_three_way(
            it1_cache_line.begin(), it1_cache_line.end(),
            it2_cache_line.begin(), it2_cache_line.end());

        if (res == std::strong_ordering::equal) {
            auto range1 = std::ranges::subrange(vec.begin() + idx1.index + idx1.cache_line_size(), vec.end());
            auto range2 = std::ranges::subrange(vec.begin() + idx2.index + idx2.cache_line_size(), vec.end());

            return std::ranges::lexicographical_compare(range1, range2);
        }

        return res == std::strong_ordering::less;
    }
};

Of course, I can implement another template specialization of this template for zero size cache, but this will lead to code duplication and I have many such functions, so I don’t want to specialize all of them.

What is a proper way in modern C++ to avoid this static hack and possible code duplication on the other hand?

I am not sure, maybe some kind of conditional code include depending on the type could help.

I would like to avoid wrapping access to cache_line to a function, but if this is the only case, please give a clue on the approach.

The compilable code is here.


Solution

  • I've added an example with a trick to use the if constexpr(...) in non-template code.

    It is now impossible to actually use the at() function accidentally, as opposed to the static member solution.

    #include <array>
    
    using data_type = int;
    
    template<size_t _data_size>
    class ExtendableIndex
    {
    public:
        constexpr static size_t data_size = _data_size;
    
        data_type& at(size_t idx) { return data[idx]; }
    
        size_t index;
        std::array<data_type, _data_size> data;
    };
    
    template<>
    class ExtendableIndex<0>
    {
    public:
        constexpr static size_t data_size = 0;
    
        data_type& at(size_t idx);
    
        size_t index;
    };
    
    using DefaultIndex = ExtendableIndex<0>;
    
    class DataUser
    {
    public:
    
        void process(DefaultIndex& index)
        {
            if constexpr (DefaultIndex::data_size > 0)
            {
                // auto value = index.data[0]; // -> this fails to compile
                auto value = index.at(0); // -> but this slight workaround solves the issue, `at()` is not implemented and thats OK.
            }
        }
    
        template<size_t _data_size>
        void process_template(ExtendableIndex<_data_size>& index)
        {
            if constexpr (DefaultIndex::data_size > 0)
            {
                auto value = index.data[0]; // -> this compiles even if index.data doesn't exist when 'process' is a template
            }
        }
    
    };
    
    int main()
    {
        DataUser r;
        ExtendableIndex<0> index_zero;
    
        r.process(index_zero);
        r.process_template(index_zero);
    
        ExtendableIndex<1> index_one;
        r.process_template(index_one);
    }