2016-08-26 2 views
6

Ich habe eine Familie von Klassen mit Methoden mit der folgenden Signatur eine generische Liste Extraktion Metaprogramm:Wie für den Aufbau eines Funktionsaufruf

double compute(list<T> pars) 

Diese Methode führt eine Berechnung mit den Parametern erhalten durch pars. Für jede compute(list) Methode habe ich eine andere compute(x1, x2, ..., xn), die die Methode ist, die die echte Berechnung implementiert. Somit sollte compute(pars) einige tun wie:

double compute(list<T> pars) 
{ 
    T x1 = list.pop_back(); 
    T x2 = list.pop_back(); 
    // .. so on until last parameter xn 
    T xn = list.pop_back(); 

    return compute(x1, x2, .., xn); // here the real implementation is called 
} 

Dieses Muster viele Male wiederholt, das einzige, was geändert werden könnte, ist die Größe der pars Liste und natürlich die Umsetzung von compute(x1, x1, ..).

Ich möchte einen Weg finden, diesen repetitiven Prozess zu "treiben"; Konkret, Extrahieren der Parameter in pars Liste und Aufbau des Anrufs zu compute(x1, x2, .., xn). Ich habe versucht, ohne Erfolg einige Makro-Tricks zu machen.

Meine Frage ist, ob es einen Weg auf metaprogramming basiert existiert, die mir erlaubt compute(list<T> pars) einmal zu implementieren und einfach wiederverwenden n, um den Anruf zu compute(x1, x2, ..., xn)

EDIT auszuführen: Dies ist die Unterschrift der anderen compute(x1, ...)

VtlQuantity compute(const VtlQuantity & x1, 
        const VtlQuantity & x2, 
        // any number of pars according the class 
        const VtlQuantity & xn) const 

‚VtlQuantity is a class representing double` ist, ihre Anteile und andere Sachen.

+0

'compute()' weiß nicht, die Größe des 'pars', diese ziemlich Regeln aus metaprogramming. P.S .: Wenn Sie es nicht mögen, die Liste jederzeit ohne triftigen Grund zu kopieren, sollte compute() 'ihren Parameter als Referenz nehmen. –

+5

Ist die Größe der Pars zur Kompilierzeit bekannt? –

+0

@VittorioRomeo: nicht ist es nicht. Die Größe ist in der Laufzeit bekannt. Aus der Anzahl der Parameter, die 'compute (x1, x2, .., xn) erhalten sollen, kann jedoch in der Kompilierzeit abgeleitet werden.' – lrleon

Antwort

1

Dies ist eine C++ 11-Lösung für das allgemeinere Problem Typ F eine Funktion oder Funktors des Aufbringens unter N Typ T Parameter und Zurückkehren Typ Ret, auf die Argumente N an aufeinanderfolgenden Positionen einiger InputIterator.

Dies gewinnt mehrere Flexibilitäten über eine Lösung durch ein Container-of- T der Argumente parametriert: -

  • Sie die Argumente von einem beliebigen N innerhalb einer Sequenz -sized Bereich extrahieren können.

  • Die Sequenz muss kein Container-of-T sein - obwohl es eine Folge von etwas sein muss, das in T konvertierbar ist.

  • Sie können die Argumente entweder vom letzten zum ersten (wie Sie) oder vom ersten zum letzten, aus den Standard-Containertypen extrahieren oder alle, die Vorwärts- und Rückwärts-Iteratoren unterstützen.

  • Sie können sogar F auf Argumente anwenden, die direkt von einem Eingabestrom verbraucht werden, ohne Zwischenextraktion.

  • Und natürlich können Sie Ihre Meinung über die Art der Sequenz ändern, in der Argumente zu liefern, ohne die funktionale Anwendung Lösung zu ändern.

Schnittstelle

template<typename Func, typename InIter, typename Stop = std::nullptr_t> 
typename function_traits<typename std::decay<Func>::type>::return_type 
invoke(Func && f, InIter it, Stop stop = Stop()); 

können Sie diese wie verwenden:

auto result = invoke(func,iter); 

func bei N aufeinanderfolgenden Positionen des Iterators iter auf die Argumente anzuwenden.

Auf diese Weise erhalten Sie keine Bereichsprüfung, N Argumente sind legitim zugänglich zu Ihrem Programm an diesen Positionen. Der Bereichsüberprüfungscode, den Sie in der Implementierung sehen, kompiliert zu nichts und wenn Sie außerhalb der Grenzen überschreiten, wird es UB geben.

Wenn Sie möchten, reichen Sie Überprüfung stattdessen Code:

auto result = invoke(func,iter,end); 

wo end einen Iterator vom selben Typ ist wie iter das Ende des verfügbaren Bereichs in der üblichen Weise begrenzen. In diesem Fall wird ein std::out_of_range ausgelöst, wenn N die Größe des Bereichs überschreitet.

Implementierung

#include <type_traits> 
#include <functional> 
#include <string> 

template<typename T> 
struct function_traits; 

template <typename Ret, typename ArgT, typename... ArgRest> 
struct function_traits<Ret(*)(ArgT, ArgRest...)> 
{ 
    static constexpr std::size_t n_args = 1 + sizeof...(ArgRest); 
    using first_arg_type = ArgT; 
    using return_type = Ret; 
}; 

template <typename Ret, typename ArgT, typename... ArgRest> 
struct function_traits<std::function<Ret(ArgT, ArgRest...)>> 
{ 
    static constexpr std::size_t n_args = 1 + sizeof...(ArgRest); 
    using first_arg_type = ArgT; 
    using return_type = Ret; 
}; 

namespace detail { 

template<typename Left, typename Right> 
typename std::enable_if<!std::is_same<Left,Right>::value>::type 
range_check(Left, Right, std::string const &){} 

template<typename Left, typename Right> 
typename std::enable_if<std::is_same<Left,Right>::value>::type 
range_check(Left start, Right end, std::string const & gripe) { 
    if (start == end) { 
     throw std::out_of_range(gripe); 
    } 
} 

template< 
    std::size_t N, typename Func, typename InIter, typename Stop, 
    typename ...Ts 
> 
typename std::enable_if< 
    N == function_traits<typename std::decay<Func>::type>::n_args, 
    typename function_traits<typename std::decay<Func>::type>::return_type 
>::type 
invoke(Func && f, InIter, Stop, Ts...args) 
{ 
    return f(args...); 
} 

template< 
    std::size_t N, typename Func, typename InIter, typename Stop, 
    typename ...Ts 
> 
typename std::enable_if< 
    N != function_traits<typename std::decay<Func>::type>::n_args, 
    typename function_traits<typename std::decay<Func>::type>::return_type 
>::type 
invoke(Func && f, InIter it, Stop stop, Ts...args) 
{ 
    range_check(it,stop, 
     "Function takes more arguments than are available " 
     "in `" + std::string(__PRETTY_FUNCTION__) + '`'); 
    using arg_type = typename 
     function_traits<typename std::decay<Func>::type>::first_arg_type; 
    auto arg = static_cast<arg_type>(*it); 
    return invoke<N + 1>(std::forward<Func>(f),++it,stop,args...,arg); 
} 

} // namespace detail 

template<typename Func, typename InIter, typename Stop = std::nullptr_t> 
typename function_traits<typename std::decay<Func>::type>::return_type 
invoke(Func && f, InIter it, Stop stop = Stop()) 
{ 
    return detail::invoke<0>(std::forward<Func>(f),it,stop); 
} 

Die beiden Spezialisierungen von function_traits<T> wird Kompilierung Funktionstypen T vorgesehen beschränken, die mindestens ein Argument, das sollte genügen für wahrscheinlich Anwendungen. Sollten Sie benötigen Aufruf auf Typen unterstützen nehmen 0 Argumente dann können Sie sie mit vermehren:

template <typename Ret> 
struct function_traits<Ret(*)()> 
{ 
    static constexpr std::size_t n_args = 0; 
    using return_type = Ret; 
}; 

template <typename Ret> 
struct function_traits<std::function<Ret()>> 
{ 
    static constexpr std::size_t n_args = 0; 
    using return_type = Ret; 
}; 

Die Spezialisierung kostenlos Funktionen function_traits<Ret(*)(ArgT, ArgRest...)>, ist ausschließlich eine redundante Bequemlichkeit, da auch sie in std::function Objekte gewickelt werden konnte , wie Sie für etwas schicker als eine freie Funktion tun müssen.

Demo

Für ein Programm, das die Funktionen besprochen Übungen, die Sie anfügen können:

#include <iostream> 
#include <list> 
#include <vector> 
#include <deque> 
#include <sstream> 
#include <iterator> 

struct num 
{ 
    double d; 
    explicit operator double() const { 
     return d; 
    } 
}; 

double add4(double d0, double d1, double d2, double d3) 
{ 
    std::cout << d0 << '+' << d1 << '+' << d2 << '+' << d3 << "\n="; 
    return d0 + d1 + d2 + d3; 
} 

int multiply2(int i0, int i1) 
{ 
    std::cout << i0 << '*' << i1 << "\n="; 
    return i0 * i1; 
} 

struct S 
{ 
    int subtract3(int i0, int i1, int i2) const 
    { 
     std::cout << i0 << '-' << i1 << '-' << i2 << "\n="; 
     return i0 - i1 - i2; 
    } 
    int compute(std::list<int> const & li) const { 
     std::function<int(int,int,int)> bind = [this](int i0, int i1, int i2) { 
      return this->subtract3(i0,i1,i2); 
     }; 
     return invoke(bind,li.begin()); 
    } 
}; 


int main() 
{ 
    std::vector<double> vd{1.0,2.0,3.0,4.0}; 
    std::vector<double> vdshort{9.0}; 
    std::list<int> li{5,6,7,8}; 
    std::deque<num> dn{num{10.0},num{20.0},num{30.0},num{40.0}}; 
    std::istringstream iss{std::string{"10 9 8"}}; 
    std::istream_iterator<int> it(iss); 
    std::cout << invoke(add4,vd.rbegin()) << '\n'; 
    std::cout << invoke(multiply2,li.begin()) << '\n'; 
    std::cout << invoke(add4,dn.rbegin()) << '\n'; 
    std::cout << invoke(multiply2,++it) << '\n'; 
    S s; 
    std::cout << '=' << s.compute(li) << '\n'; 
    try { 
     std::cout << invoke(add4,vdshort.begin(),vdshort.end()) << '\n'; 
    } catch(std::out_of_range const & gripe) { 
     std::cout << "Oops :(\n" << gripe.what() << '\n'; 
    } 

    return 0; 
} 

Der Fall:

S s; 
    std::cout << '=' << s.compute(li) << '\n'; 

ist besonders relevant für Ihr spezielles Problem, da Hier rufen wir S::compute(std::list<int> const & li), um eine andere nicht-statische Methodeanzuwendenvon S zu den Argumenten, die in der Liste li geliefert werden. Siehe in der Umsetzung von S::compute, wie die Verwendung einer Lambda bequem sowohl die Aufruf S Objekt und S::compute in eine std::function binden können wir können Pass zu invoke.

Live demo

1

C++ 17 Lösung unten. wandbox link

(Erheblich vereinfacht dank Jarod42)

  • nimmt die Anzahl der Argumente N zur Compile-Zeit bekannt ist, aber die Liste eine beliebige Größe haben kann.

  • Dies ruft pop_back() mehrmals wie im Beispiel gezeigt, ruft dann eine Funktion auf.


template <typename T> 
struct list 
{ 
    T pop_back() { return T{}; } 
}; 

namespace impl 
{  
    template<typename TList, std::size_t... TIs> 
    auto list_to_tuple(TList& l, std::index_sequence<TIs...>) 
    { 
     using my_tuple = decltype(std::make_tuple((TIs, l.pop_back())...)); 
     return my_tuple{((void)TIs, l.pop_back())...}; 
    } 
} 

template<std::size_t TN, typename TList> 
auto list_to_tuple(TList& l) 
{ 
    return impl::list_to_tuple(l, std::make_index_sequence<TN>()); 
} 

template <std::size_t TN, typename TList, typename TF> 
auto call_with_list(TList& l, TF&& f) 
{ 
    return std::experimental::apply(f, list_to_tuple<TN>(l)); 
} 

void test_compute(int, int, int) 
{ 
    // ... 
} 

int main() 
{ 
    list<int> l{}; 
    call_with_list<3>(l, test_compute); 
} 

Wie funktioniert es?

Die Idee ist, dass wir „konvertieren“, um eine Liste zu einem Tupel, der angibt, wie viele Elemente, die wir aus der Liste zur Compile-Zeit Pop wollen list_to_tuple<N>(list) verwenden.

Nachdem ein Tupel aus der Liste abgerufen wurde, können wir std::experimental::apply verwenden, um eine Funktion aufzurufen, indem wir die Elemente des Tupels als Argumente verwenden: Dies geschieht durch call_with_list<N>(list, func).

Um ein Tupel aus der Liste zu erstellen, brauchen zwei Dinge zu tun:

  1. ein std::tuple<T, T, T, T, ...> Erstellen, wo TN mal wiederholt.

  2. Rufen Sie list<T>::pop_back()N mal, setzen Sie die Elemente in das Tupel.

das erste Problem zu lösen, wird decltype verwendet, um den Typen der folgenden variadische Expansion zu bekommen: std::make_tuple((TIs, l.pop_back())...). Der Komma-Operator wird verwendet, damit TIs, l.pop_back() zu decltype(l.pop_back()) ausgewertet wird.

Um das zweite Problem zu lösen, wird innerhalb des std::initializer_list Tupelkonstruktors eine variable Erweiterung verwendet, die die Reihenfolge der Auswertung garantiert: return my_tuple{((void)TIs, l.pop_back())...};. Der gleiche oben beschriebene Komma-Operator "Trick" wird hier verwendet.


Kann ich schreibe es in C++ 11?

Ja, aber es wird etwas "nerviger" sein.

  • std::experimental::apply ist nicht verfügbar: online suchen nach Lösungen like this one.

  • std::index_sequence ist nicht verfügbar: Sie müssen Ihre eigenen implementieren.

+0

'mit my_tuple = gepostet decltype (std :: make_tuple ((Is, l.pop_back()) ...)); '(unbestimmte Reihenfolge, aber ändert den Typ nicht) und' my_tuple res {(Is, l.pop_back()). ..}; '(' initializer_list' force order evaluation) scheint einfacher zu sein. – Jarod42

+0

@ Jarod42: gute Punkte, ich werde die Antwort verbessern - danke. Jede einfache Art der Evaluierungsreihenfolge könnte in 'make_tuple' erzwungen werden, um' always_t' zu vermeiden und nur 'return std :: make_tuple (((void) TIs, l.pop_back()) ...);'? –

+0

Nein mit Funktionsaufruf als 'make_tuple', aber wie gesagt, wir können es verwenden, um den Typ zu kennen, und dann einfach' initializer_list' zu verwenden. – Jarod42

3

können Sie wie folgt vorgehen:

template <typename Func, typename T, std::size_t ... Is> 
decltype(auto) apply(Func&& f, const std::list<T>& pars, std::index_sequence<Is...>) 
{ 
    std::vector<T> v(pars.rbegin(), pars.rend()); 

    return std::forward<Func>(f)(v.at(Is)...); 
} 

template <std::size_t N, typename Func, typename T> 
decltype(auto) apply(Func&& f, const std::list<T>& pars) 
{ 
    return apply(std::forward<Func>(f), pars, std::make_index_sequence<N>()); 
} 

Mit Nutzung ähnlich wie:

apply<6>(print, l); 

Demo

Um automatisch die arity der Funktion zu berechnen, Sie erstellen können Merkmale:

template <typename F> struct arity; 

template <typename Ret, typename ...Args> struct arity<Ret(Args...)> 
{ 
    static constexpr std::size_t value = sizeof...(Args); 
}; 

und dann

template <typename Func, typename T> 
decltype(auto) apply(Func&& f, const std::list<T>& pars) 
{ 
    constexpr std::size_t N = arity<std::remove_pointer_t<std::decay_t<Func>>>::value; 
    return apply(std::forward<Func>(f), pars, std::make_index_sequence<N>()); 
} 

Demo

Sie haben arity zu bereichern zu unterstützen Functor (als Lambda).

+0

Danke für Ihr Interesse. Gibt es eine Möglichkeit, dem Compiler die Anzahl der Parameter zu erklären? Einige, die einen Funktionsnamen sehen und daraus errechnen, wie viele Parameter er erhält. Ich denke, das wäre das einzige, was fehlt. – lrleon

+1

@lrleon Entferne 'N', ersetze' Func' durch 'R' und' Args ... 'und benutze' R f (Args ...) 'und benutze' sizeof ... (Args) 'anstelle von' N '. – Holt

+1

Es wäre besser, ein Array zu verwenden und die zusätzliche dynamische Zuweisung zu vermeiden, aber das ist ein Trick, also denke ich nicht. Sie könnten auch den Vektor vorreservieren und dann die Elemente einfügen, was ein wenig performanter sein könnte. – Yakk

1
template<class T> using void_t = void; 

template<class T, class F, std::size_t N=0, class=void> 
struct arity:arity<T, F, N+1> {}; 

template<class F, class T, class Indexes> 
struct nary_result_of{}; 

template<std::size_t, class T> 
using ith_T=T; 

template<class F, class T, std::size_t...Is> 
struct nary_result_of<F, T, std::index_sequence<Is...>>: 
    std::result_of<F(ith_T<Is, T>)> 
{}; 

template<class T, class F, std::size_t N> 
struct arity<T, F, N, void_t< 
    typename nary_result_of<F, T, std::make_index_sequence<N>>::type 
>>: 
    std::integral_constant<std::size_t, N> 
{}; 

arity verwendet eine C++ 14-Funktion (Indexsequenzen, leicht C++ 11 zu schreiben, in).

Es dauert Arten F und ein T und sagt Ihnen, die geringste Anzahl von T s Sie F passieren kann der Anruf gültig zu machen.Wenn sich keine Zahl von T qualifiziert, wird Ihr Template-Instanziierungs-Stack durchgebrannt und Ihr Compiler beklagt sich oder stirbt.

template<class T> 
using strip = typename std::remove_reference<typename std::remove_cv<T>::type>::type; 

namespace details { 
    template<class T, std::size_t N, class F, class R, 
    std::size_t...Is 
    > 
    auto compute(std::index_sequence<Is...>, F&& f, R&& r) { 
    std::array<T, N> buff={{ 
     (void(Is), r.pop_back())... 
    }}; 
    return std::forward<F>(f)(buff[Is]...); 
    } 
} 
template<class F, class R, 
    class T=strip< decltype(*std::declval<R&>().begin()) > 
> 
auto compute(F&& f, R&& r) { 
    return details::compute(std::make_index_sequence<arity<F,T>{}>{}, std::forward<F>(f), std::forward<R>(r)); 
} 

Das einzige, was wirklich ärgerlich zu konvertieren, um C++ 11 der auto Rückgabetyp auf compute ist. Ich müsste meine arity umschreiben.

Diese Version sollte die Arität selbst von Nichtfunktionszeigern automatisch erkennen, so dass Sie sie mit lambdas oder std::function s aufrufen können oder was Sie haben.

Verwandte Themen