How to unify access to class member when their presense is optional?

68 Views Asked by At

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.

My first idea was to introduce a dummy static std::array in the specialization for the struct (demo):

#include <array>

using data_type = int;

template<std::size_t _data_size>
struct ExtendableIndex
{
    std::size_t index;
    std::array<data_type, _data_size> data;
};

template<>
struct ExtendableIndex<0>
{
    std::size_t index;
    constexpr static std::array<data_type, 0> data;     // Dummy static object to make access to data transparent
};

constexpr std::array<data_type, 0> ExtendableIndex<0>::data;

constexpr std::size_t cache_length = 0;     // Can be set to any cache size including 0
using DefaultIndex = ExtendableIndex<cache_length>;
     
void data_user(const DefaultIndex& index)
{

    auto value = index.data.begin(); // -> this won't compile for ExtendableIndex<0> without dummy static object
}

int main()
{
    ExtendableIndex<cache_length> index_one;
    data_user(index_one);
}

which works fine and has the advantage of transparency for all data_user-type algorithms in work with data field regardless of the presence of real data.

Aedoro in his answer to my question What is the approach to handle optional class members? provided much better solutions (demo):

#include <array>

using data_type = int;

template<std::size_t _data_size>
class ExtendableIndex
{
public:
    constexpr static std::size_t data_size = _data_size;

    data_type& at(std::size_t idx) { return data[idx]; }

    std::size_t index;
    std::array<data_type, _data_size> data;
};

template<>
class ExtendableIndex<0>
{
public:
    constexpr static std::size_t data_size = 0;

    data_type& at(std::size_t idx);

    std::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<std::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);
}

which avoid creation of dummy static member and solves the task either with a function or with making data_user functions templates. I like his solution and still consider it much better than mine, but it lacks the transparency of my solution. When it comes to large data_user with extensive usage of data field, I have to write many if constexpr to make different code branches and this additionally hits complex conditions which rely on lazy evaluation of || and ‘&&` operations.

The question is, is there a third way? Namely, could this be solved transparently like in my case, without dummy static object and preferably without wrapping access to data field in a separate method?

I know that one of correct answers would be to make functional decomposition for data_user functions, extract code which works with data and make template with if constexpr, but sometimes it is hard and leads to code duplication, anyway if I want to keep lazy evaluation of || and ‘&&` operations.

1

There are 1 best solutions below

3
Jarod42 On

Similarly to your static member hack, but relying on compiler, since C++20, you might use attribute [[no_unique_address]]:

template<std::size_t _data_size>
struct ExtendableIndex
{
    std::size_t index;
    [[no_unique_address]] std::array<data_type, _data_size> data;
};

so sizeof(ExtendableIndex<0>) == sizeof(std::size_t) might be true, (but is not guaranteed :-/

You can ensure compiler support it as intended with static_assert it

static_assert(sizeof(ExtendableIndex<0>) == sizeof(std::size_t),
              "Compiler doesn't support [[no_unique_address]] as intended");

EBO is guaranteed to some extends, and is generally more supported even for no-guaranteed case, so

template<std::size_t _data_size>
struct ExtendableIndex : private std::array<data_type, _data_size>
{
    std::size_t index;
    std::array<data_type, _data_size>& data() { return *this; }
    const std::array<data_type, _data_size>& data() const { return *this; }
};

Unfortunately, std::array<T, 0> implementations seems not compatible... You might probably provide your own empty_array to fix that.