How to avoid using the "indices trick" repeatedly?

492 Views Asked by At

I have a class named memory_region, which is sort of like an untyped gsl::span (i.e. it's essentially a void* and a size_t), which I also use for type erasure. It thus has an as_span<T>() method.

With this class, I have a std::unordered_map<std::string, memory_region> my_map - which is used to pass type-erased spans between parts of my code which don't share headers, so they can't know about each others' types. The typical access to one of these looks like:

auto foo = my_map.at("foo").as_span<bar_t>();

This works just fine with code that has a fixed set of buffers and types and names. But - things get tricky when my code's buffers depend on a template parameter pack. Now, I've implemented a

std::string input_buffer_name(unsigned input_buffer_index);

function, so if I have an index sequence and my parameter pack I can do, for example

template<typename Ts..., std::size_t... Indices>
my_function(std::unordered_map<std::string, memory_region>& my map) {
    compute_stuff_with_buffers(
        my_map.at(input_buffer_name(Indices)).as_span<Ts>()...
    );
}

(this is a variation on the infamous indices trick; note that the same type may appear more than once in the pack, so I can't "wrap the types in a tuple" and acces it by type.)

The thing is, though - my code doesn't have that index sequence in the template parameters; most of it is templated on just the parameter pack of types. So I find myself writing "helper functions/methods" all the time to be able to use that index sequence, e.g.:

template<typename Ts..., std::size_t... Indices>
my_function_helper(
    std::unordered_map<std::string, memory_region>& my map
    std::index_sequence<Indices...>  /* unused */) 
{
    compute_stuff_with_buffers(
        my_map.at(input_buffer_name(Indices)).as_span<Ts>()...
    );
}

template<typename Ts...>
my_function(std::unordered_map<std::string, memory_region>& my map) {
    my_function_helper(
        my_map, std::make_index_sequence<sizeof...(Ts)> {}
    );
}

What can I do instead, that will not involve so much code duplication?

1

There are 1 best solutions below

2
AndyG On

In this case you can use simple pack expansion in the form of an array:

template<typename... Ts>
void my_function(std::unordered_map<std::string, memory_region>& my_map) {
    using swallow = int[];
    unsigned i = 0;
    (void)swallow{0, (my_map.at(input_buffer_name(i++)).as_span<Ts>(), 0)...};
}

Demo

The pack expansion will be expanded in order ([temp.variadic]), and also evaluated in order (left to right) because we're using a braced initializer list (an unused integer array): [dcl.init.aggr]

When an aggregate is initialized by an initializer list [...] the elements of the initializer list are taken as initializers for the elements of the aggregate, in order.


Re:

But what if I need to use input_buffer_name(i) twice? e.g. if I need to use { input_buffer_name(index), my_map.at(input_buffer_name(index).as_span<Ts>()) }

I suppose we could take advantage of the fact that logical AND will sequence left to right ([expr.log.and]), and also a boolean can be promoted to int:

template<typename... Ts>
void my_function_v2(std::unordered_map<std::string, memory_region>& my_map) {
    using swallow = int[];
    unsigned i = 0;
    (void)swallow{0, ((std::cout<< input_buffer_name(i) << std::endl, true) && (my_map.at(input_buffer_name(i++)).as_span<Ts>(), true))...};
}

Demo 2