I have the following function which can take N arguments of different types, and forwards them to N functions templated on each individual type, in this manner (example with
Here's a solution similar to max's, but it: a) clearly separates out the generic parts from the parts specific to the solution, and b) I show that clang fully optimizes it. The basic idea is to build a switch case at compile time, from a contiguous integer sequence. We do that like this:
template
auto compile_switch(T i, std::integer_sequence, F f) {
using return_type = std::common_type_t{}))...>;
return_type ret;
std::initializer_list ({(i == Is ? (ret = f(std::integral_constant{})),0 : 0)...});
return ret;
}
The idea is that integer gets passed into the lambda as an integral constant type, so its usable in compile time contexts. To use this with the current problem, all we have to do is forward the variadic pack a tuple, and apply the usual tricks with index sequence:
template
bool func_impl(std::size_t& counter, T&& t, std::index_sequence is) {
auto b = compile_switch(counter, is, [&] (auto i) -> bool {
return func2(std::get(std::move(t)));
});
if (b) ++counter;
return b;
}
template
bool func(std::size_t & counter, Ts&& ... ts) {
return func_impl(counter,
std::forward_as_tuple(std::forward(ts)...),
std::index_sequence_for{});
}
We'll use this definition of func2 to look at some assembly:
template
bool func2(const T& t) { std::cerr << t; return std::is_trivial::value; }
Looking here: https://godbolt.org/g/6idVPS we notice the following instructions:
auto compile_switch, std::allocator > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple, std::allocator > const&>&&, std::integer_sequence)::{lambda(auto:1)#1}>(unsigned long, std::integer_sequence, bool func_impl, std::allocator > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple, std::allocator > const&>&&, std::integer_sequence)::{lambda(auto:1)#1}): # @auto compile_switch, std::allocator > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple, std::allocator > const&>&&, std::integer_sequence)::{lambda(auto:1)#1}>(unsigned long, std::integer_sequence, bool func_impl, std::allocator > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple, std::allocator > const&>&&, std::integer_sequence)::{lambda(auto:1)#1})
push r14
push rbx
push rax
mov bl, 1
cmp rdi, 5
ja .LBB2_11
jmp qword ptr [8*rdi + .LJTI2_0]
Looking down for that label, we find:
.LJTI2_0:
.quad .LBB2_2
.quad .LBB2_4
.quad .LBB2_5
.quad .LBB2_6
.quad .LBB2_7
.quad .LBB2_10
In other words, clang has both turned this into a jump table, and inlined all the calls to func2. This is not possible using a table of function pointers as some have suggested (at least I've never seen a compiler do it), in fact the only way to get assembly this good is via switch case, or with this technique + clang. Sadly gcc will not generate assembly quite as good, but still decent.