Variadic templates and switch statement?

我怕爱的太早我们不能终老 提交于 2019-12-02 19:11:47

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 <class T, T ... Is, class F>
auto compile_switch(T i, std::integer_sequence<T, Is...>, F f) {
  using return_type = std::common_type_t<decltype(f(std::integral_constant<T, Is>{}))...>;
  return_type ret;
  std::initializer_list<int> ({(i == Is ? (ret = f(std::integral_constant<T, Is>{})),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 <class T, std::size_t ... Is>
bool func_impl(std::size_t& counter, T&& t, std::index_sequence<Is...> is) {
  auto b = compile_switch(counter, is, [&] (auto i) -> bool {
    return func2(std::get<i>(std::move(t)));
  });
  if (b) ++counter;
  return b;
}

template <class ... Ts>
bool func(std::size_t & counter, Ts&& ... ts) {
  return func_impl(counter,
      std::forward_as_tuple(std::forward<Ts>(ts)...),
      std::index_sequence_for<Ts...>{});
}

We'll use this definition of func2 to look at some assembly:

template <class T>
bool func2(const T& t) { std::cerr << t; return std::is_trivial<T>::value; }

Looking here: https://godbolt.org/g/6idVPS we notice the following instructions:

auto compile_switch<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul, bool func_impl<std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>)::{lambda(auto:1)#1}>(unsigned long, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>, bool func_impl<std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>)::{lambda(auto:1)#1}): # @auto compile_switch<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul, bool func_impl<std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>)::{lambda(auto:1)#1}>(unsigned long, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>, bool func_impl<std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>(unsigned long&, std::tuple<int&, double&, int&, unsigned long&, char const*&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul>)::{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.

Just for fun, I propose the following way

template <typename ... Ts>
bool func (int & cnt, Ts ... xs)
 {
   using unused = int[];

   int  i   { -1 };
   bool ret { true };

   (void)unused { 0, ((++i == cnt ? (ret = func<Ts>(xs)) : true), 0)... };

   if ( ret && (cnt <= i) )
      ++cnt;

   return ret;
 }

but I don't think is a efficient way as the switch way.

This solution may be the most efficient:

template<size_t I,class...Args>
bool func2_b(Args...arg)
  {
  if (func2(std::get<I>(std::tuple<Args...>{arg...})))
    return true;
  else
   return false;
  }

template<class...Args,size_t...Is>
bool func_(int& counter,std::index_sequence<Is...>,Args...args)
  {
  using ft = bool(*)(Args...);
  ft table[]={func2_b<Is,Args...>...};
  if (counter<0 || counter>=(int)sizeof...(Args))
    return false;
  return table[counter](args...);
  }

template<class...Args>
bool func(int& counter,Args...xs)
  {
  return func_(counter,std::make_index_sequence<sizeof...(Args)>{},xs...);
  }

Also for fun, this might be a bit too convoluted

#include<type_traits>
#include<array>

template<typename T>
void g(T&& t)
{
    // This function gets called
}

template<typename T>
void entry(void* p)
{
    g(*(std::remove_reference_t<T>*)p);
}

template<size_t N>
using table_t = std::array<void (*)(void*), N>;

template<typename... Ts>
constexpr auto make_table()
{
    return table_t<sizeof...(Ts)>{
        entry<Ts>...
    };
}

template<size_t N>
void f_(const table_t<N>&, int)
{

}

template<size_t N, typename T, typename... Ts>
void f_(const table_t<N>& table, int select, T&& t, Ts&&... ts)
{
    if(select == N - sizeof...(Ts) - 1)
        table[select]((void*)&t);
    else
        f_(table, select, std::forward<Ts>(ts)...);
}

template<typename... Ts>
void f(int select, Ts&&... ts)
{
    static constexpr auto table = make_table<Ts...>();
    if(select < 0 || select >= int(sizeof...(Ts)))
        throw "out of bounds";
    f_(table, select, std::forward<Ts>(ts)...);
}

Which rolls a vtable in f and dispatches accordingly to g.

Live

Theoretically you could do binary search of parameter index by yourself:

#include <type_traits>
#include <tuple>
#include <typeinfo>
#include <iostream>
#include <algorithm>


template <std::size_t I>
using ic = std::integral_constant<std::size_t, I>;

template <class T>
bool func2(T) {
    std::cout<<typeid(T).name()<<std::endl;
    return true;
}

template <std::size_t N, class T>
bool func_impl(ic<0>, ic<N>, std::size_t &, T &&tup) {
    constexpr int index = std::min(N - 1, std::tuple_size<T>{} - 1);
    if (func2<std::tuple_element_t<index, std::decay_t<T>>>(std::get<index>(tup))) 
        return true;
    return false;
}

template <std::size_t K, std::size_t N, class T>
bool func_impl(ic<K>, ic<N> n, std::size_t &counter, T &&tup) {
    if (counter == N - 1) {
        return func_impl(ic<0>{}, n, counter, std::forward<T>(tup));
    }
    if (counter < N) {
        return func_impl(ic<K/2>{}, ic<N - K>{}, counter, std::forward<T>(tup));
    } else {
        return func_impl(ic<K/2>{}, ic<N + K>{}, counter, std::forward<T>(tup));
    }
}

template <class... Ts>
bool func(std::size_t& counter, Ts&&... xs) {
    return func_impl(ic<sizeof...(Ts)/2>{}, ic<sizeof...(Ts)/2>{}, counter, std::forward_as_tuple(xs...));
}

int main() {
   std::size_t i = 0;
   func<int, float, double, char>(i, 1, 2, 3, 4); 
}

[live demo]

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!