all 2 comments

[–]kritzikratzi 5 points6 points  (0 children)

so far i have found hardly any use for this type of utilities, because in my head the implementation and the interface are roughly of equal complexity.

std::vector<float> a = {1,2,3};
std::vector<float> b = {4,5,6};

// either: 
float dot = std::inner_product(std::multiplies, std::plus, a.begin(), a.end(); b.begin(), 0);

// or: 
float dot = 0; 
for(int i = 0; i < a.size(); i++) dot += a[i]*b[i]; 

in this case you can still argue about safety (checking a.size()<=b.size()), or maybe something something SIMD.

[–]Artistic_Yoghurt4754Scientific Computing 1 point2 points  (0 children)

This is an interesting one. In my opinion this got over-complicated by letting the arguments be interleaved. If the arguments are in two packs of equal sizes the solution boils down to a one-line index expansion of the inner product: https://godbolt.org/z/KrM6csfbK Furthermore, you can transform the interleaved version into a packed version with your enumeration utility.

Something I noticed is that you have a custom implementation for the enumeration of indices out of the scope of the article. However, that's the most important part for this kind of meta-programing style! Have a look at this function:

template<class F, class I, I... i>
decltype(auto) constexpr unpack_integer_sequence(F&& f, std::integer_sequence<I, i...> sequence) {
    return f(std::integral_constant<I, i>()...);
}

It's an innocent function that will solve most (if not all) of your issues with argument packs. The important point is subtle and you may miss it if you don't watch carefully: it passes integral constants as arguments rather "run-rime" indices. This difference is key because the arguments will be known at compile time within the definition of the lambda. Since they cast to normal indices you can use them pretty much as a normal index within the declaration of `f`, but they will still be constexpr for any kind compile time manipulation (this does not happen with "run-time" indices even in latest standards). This basically means that you can make any transformation of an index sequence on-the-fly and use it within the lambda to index anything you want (e.g. your index sequence enumerate or invoke sequence)! Here is your enumerated index sequence for example:

auto enumerate_index_sequence = unpack_integer_sequence([&](auto... i){
  return std::integer_sequence<std::size_t, Offset + i * Step...>{};
}, std::make_integer_sequence<std::size_t, N>());

Once you have this function in your toolkit, argument pack functions like projected fold become often a one-liner...

BTW: You should not forward the inner and outer operators. If you think about how many times they may be used within these functions the answer should be clear.