web-dev-qa-db-de.com

Variadische Vorlagen und switch-Anweisung?

Ich habe die folgende Funktion, die N Argumente unterschiedlichen Typs annehmen und auf diese Weise an N Funktionen weiterleiten kann, die für jeden einzelnen Typ als Vorlage dienen (Beispiel mit zwei Argumenten):

template <typename T1, typename T2>
bool func(int& counter, T1 x1, T2 x2) {
    switch (counter) {
        case 0:
            if (func2<T1>(x1)) {
                counter++;
                return true;
            } else {
                return false;
            }
        case 1:
            if (func2<T2>(x2)) {
                counter++;
                return true;
            } else {
                return false;
            }
        default:
            return true;
    }
}

Ich möchte diese Funktion mit verschiedenen Vorlagen schreiben, damit sie eine beliebige Anzahl von Argumenten typsicher verarbeiten kann. Ich sehe eine Lösung, die rekursive Funktionen verwendet, den Zähler und den variadischen Index durchläuft und sie auf Gleichheit vergleicht, aber dies scheint einen weitaus weniger effizienten Code zu ergeben als die obige switch-Anweisung (eine Folge von if-Prüfungen im Vergleich zu einer Sprungtabelle) ).

Kann dies mithilfe der Metaprogrammierung von Vorlagen effizient durchgeführt werden oder muss ich für jede Arity eine Überladung bereitstellen?

16
Thomas

Hier ist eine Lösung, die der von max ähnlich ist, aber es gilt: a) trennt die generischen Teile klar von den für die Lösung spezifischen Teilen, und b) ich zeige, dass Clang sie vollständig optimiert. Die Grundidee besteht darin, zur Kompilierzeit aus einer zusammenhängenden ganzzahligen Sequenz einen Wechselfall zu erstellen. Das machen wir so:

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;
}

Die Idee ist, dass Integer als integraler Konstantentyp an das Lambda übergeben wird, sodass sie im Kontext der Kompilierung verwendet werden kann. Um dies mit dem aktuellen Problem zu verwenden, müssen wir dem Variadic Pack nur ein Tuple weiterleiten und die üblichen Tricks mit der Indexsequenz anwenden:

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...>{});
}

Wir werden diese Definition von func2 verwenden, um einige Assemblys anzusehen:

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

Wenn Sie hier suchen: https://godbolt.org/g/6idVPS beachten Sie die folgenden Anweisungen:

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]

Wenn wir nach diesem Label suchen, finden wir:

.LJTI2_0:
        .quad   .LBB2_2
        .quad   .LBB2_4
        .quad   .LBB2_5
        .quad   .LBB2_6
        .quad   .LBB2_7
        .quad   .LBB2_10

Mit anderen Worten, clang hat dies in eine Sprungtabelle verwandelt, und alle Aufrufe von func2 inline. Dies ist nicht möglich, wenn eine Tabelle mit Funktionszeigern verwendet wird, wie einige vorgeschlagen haben (zumindest habe ich noch nie einen Compiler gesehen). In der Tat ist die einzige Möglichkeit, Assembly so gut zu bekommen, über das Switch-Case oder mit dieser Technik + Clang. Leider wird GCC Assembly nicht ganz so gut erzeugen, aber immer noch anständig.

13
Nir Friedman

Nur zum Spaß schlage ich den folgenden Weg vor

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;
 }

aber ich denke nicht, dass es ein effizienter Weg ist, um den Weg zu wechseln.

3
max66

Diese Lösung kann die effizienteste sein:

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...);
  }
2
Oliv

Auch zum Spaß ist das vielleicht etwas zu kompliziert

#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)...);
}

Würfelt eine vtable in f und sendet entsprechend an g.

Leben

1
Passer By

Theoretisch können Sie die binäre Suche des Parameterindex selbst durchführen:

#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]

0
W.F.