2016-08-26 2 views
6

J'ai une famille de classes avec des méthodes avec la signature suivante:Comment métaprogramme une extraction de liste générique pour la construction d'une fonction d'appel

double compute(list<T> pars) 

Cette méthode effectue un calcul avec les paramètres reçus par pars. Pour chaque méthode compute(list), j'ai un autre compute(x1, x2, ..., xn) qui est la méthode implémentant le calcul réel. Ainsi, compute(pars) devrait faire un peu comme:

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 
} 

Ce motif se répète à plusieurs reprises, la seule chose qui pourrait changer la taille de la liste pars et bien sûr la mise en œuvre de compute(x1, x1, ..).

Je voudrais trouver un moyen de "driisser" ce processus répétitif; Concrètement, extraire les paramètres dans la liste pars et construire l'appel à compute(x1, x2, .., xn). J'ai essayé sans succès de faire quelques trucs de macro.

Ma question est de savoir s'il existe une certaine façon basée sur metaprogramming qui me permet de mettre en œuvre compute(list<T> pars) une fois et réutiliser simplement n afin d'effectuer l'appel à compute(x1, x2, ..., xn)

EDIT: Ceci est la signature de l'autre 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 années double`, leurs unités et d'autres choses.

+0

'compute()' ne connaît pas la taille de 'pars', ces règles à peu près hors métaprogrammation. P.S .: sauf si vous aimez copier 'list's tout le temps sans raison valable,' compute() 'devrait prendre son paramètre par référence. –

+5

La taille des pars est-elle connue à la compilation? –

+0

@VittorioRomeo: ce n'est pas le cas. La taille est connue en temps courant. Cependant, il pourrait être déduit dans le temps de compilation du nombre de paramètres qui recevront 'calcul (x1, x2, .., xn)' – lrleon

Répondre

1

Ceci est un C solution ++ 11 pour le type de problème plus général de l'application une fonction ou foncteur F, en prenant N de type T paramètres et le retour de type Ret, les N arguments à des positions successives de certains itérateur d'entrée.

Cette flexibilité gagne plusieurs sur une solution paramétrés par un conteneur-of-T des arguments: -

  • Vous pouvez extraire les arguments d'une N arbitraire plage -sized dans une séquence.

  • La séquence n'a pas besoin d'être un conteneur de T - bien que ce soit une séquence de quelque chose convertible en T.

  • Vous pouvez extraire les arguments en avant-dernier (comme vous le faites) ou en avant-dernier, à partir des types de conteneur standard ou de ceux prenant en charge les itérateurs directs et inverses. Vous pouvez même appliquer F aux arguments consommés directement à partir de certains flux d'entrée, sans extraction intermédiaire .

  • Et bien sûr, vous pouvez changer d'avis sur le type de séquence dans lequel pour fournir des arguments sans avoir à changer la solution de l'application fonctionnelle.

Interface

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()); 

Vous pouvez utiliser cela comme:

auto result = invoke(func,iter); 

appliquer func aux arguments à N positions successives du iterator iter.

De cette façon, vous n'obtenez aucune vérification de portée pour que les arguments N soient légitimement accessibles à votre programme à ces positions. Le code de vérification de plage que vous allez trouver dans l'implémentation sera compilé à rien et si vous empiétez sur des limites il y aura UB.

Si vous voulez aller vérifier vous pouvez à la place du code:

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

end est un itérateur du même type que iter délimitant l'extrémité de la gamme disponible de la manière habituelle. Dans ce cas, un std::out_of_range sera envoyé si N dépasse la taille de la plage.

mise en œuvre

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

Les deux spécialisations de function_traits<T> fournies limiteront compilation aux types fonctionnels T qui prennent au moins un argument qui devrait suffire pour les applications probables. Si vous avez besoin de soutenir appel sur les types prenant 0 arguments, vous pouvez les augmenter avec:

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

La spécialisation des fonctions libres function_traits<Ret(*)(ArgT, ArgRest...)>, est strictement une commodité redondante, car ils pourraient aussi être enveloppés dans std::function objets , comme vous êtes obligé de faire pour tout ce qui est plus fantaisiste qu'une fonction gratuite.

Démo

Pour un programme qui exerce les fonctions discutées vous pouvez ajouter:

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

Le cas de:

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

est particulièrement pertinente à votre problème particulier, étant donné que ici, nous appelons S::compute(std::list<int> const & li) pour appliquer une autre méthode non statique de S aux arguments livrés dans la liste li. Voir dans la mise en œuvre de S::compute comment l'utilisation d'un lambda peut commodément se lier à la fois l'objet appelant S et S::compute dans un std::function nous pouvons passer à invoke.

Live demo

1

Solution C++ 17 ci-dessous. wandbox link

(simplifié grâce à Grandement Jarod42)

  • le nombre d'Suppose que les arguments N est connu à la compilation, mais la liste peut avoir toute taille.

  • Ceci appelle pop_back() plusieurs fois comme indiqué dans l'exemple, puis appelle une fonction.


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

Comment ça marche?

L'idée est que nous « convertir » une liste à un tuple, en précisant le nombre d'éléments que nous voulons pop dans la liste à la compilation en utilisant list_to_tuple<N>(list).

Après avoir obtenu un tuple dans la liste, nous pouvons utiliser std::experimental::apply pour appeler une fonction en appliquant les éléments du tuple comme arguments: ceci est fait par call_with_list<N>(list, func).

Pour créer un tuple dans la liste, deux choses qui doit être fait:

  1. Création d'un std::tuple<T, T, T, T, ...>, où T est répété N fois.

  2. Appelez list<T>::pop_back()N fois, en mettant les éléments dans le tuple.

Pour résoudre le premier problème, decltype est utilisé pour obtenir le type de l'expansion variadique suivante: std::make_tuple((TIs, l.pop_back())...). L'opérateur de virgule est utilisé de sorte que TIs, l.pop_back() évalue à decltype(l.pop_back()).

Pour résoudre le second problème, une extension variadique est utilisée dans le constructeur de tuple std::initializer_list, ce qui garantit l'ordre d'évaluation: return my_tuple{((void)TIs, l.pop_back())...};. Le même "trick" de l'opérateur virgule décrit ci-dessus est utilisé ici.


Puis-je écrire en C++ 11?

Oui, mais ce sera légèrement plus "ennuyeux".

  • std::experimental::apply n'est pas disponible: regarder en ligne des solutions like this one.

  • std::index_sequence n'est pas disponible: vous devrez implémenter la vôtre.

+0

'using my_tuple = decltype (std :: make_tuple ((Is, l.pop_back()) ...); '(ordre non spécifié, mais ne change pas le type) et' my_tuple res {(Is, l.pop_back()). ..}; '(' initializer_list' évaluation de l'ordre des forces) semble être plus simple. – Jarod42

+0

@ Jarod42: bons points, je vais améliorer la réponse - merci. N'importe quel ordre d'évaluation simple peut être forcé dans 'make_tuple' pour éviter' always_t' et avoir seulement 'return std :: make_tuple (((void) TIs, l.pop_back()) ... '; –

+0

Non avec l'appel de fonction comme 'make_tuple', mais comme je l'ai dit, nous pouvons l'utiliser pour connaître le type, et ensuite utiliser' initializer_list'. – Jarod42

3

Vous pouvez effectuer les opérations suivantes:

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

Avec utilisation similaire à:

apply<6>(print, l); 

Demo

Pour calculer automatiquement l'arité de la fonction, vous pouvez créer un traits:

template <typename F> struct arity; 

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

puis

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

Vous devez enrichir arity pour soutenir Functor (comme le lambda).

+0

Merci pour votre intérêt. Est-il possible d'expliquer au compilateur le nombre de paramètres? Quelqu'un qui regarde un nom de fonction et déduit combien de paramètres il reçoit. Je pense que ce serait la seule chose qui manque. – lrleon

+1

@lrleon Supprimer 'N', remplacer' Func' par 'R' et' Args ... 'et utiliser' R f (Args ...) 'et utiliser' sizeof ... (Args) 'au lieu de' N '. – Holt

+1

Il serait préférable d'utiliser un tableau et d'éviter l'allocation dynamique supplémentaire, mais c'est compliqué, donc je suppose que non. Vous pouvez également pré-réserver le vecteur puis insérer les éléments, ce qui pourrait être un peu plus performant. – 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 utilise une 14 fonctionnalité C++ (séquences d'index, facile à écrire en 11 C++).

Il faut types F et un T et vous indique le nombre minimum de T s vous pouvez passer à F pour effectuer l'appel valide.Si aucun numéro de T ne remplit les conditions requises, il bloque la pile d'instanciation de votre modèle et votre compilateur se plaint ou meurt.

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

La seule chose vraiment gênant pour convertir en C++ 11 est le type auto de retour sur compute. Je devrais réécrire mon arity.

Cette version devrait détecter automatiquement l'arité même des pointeurs de non-fonction, vous laissant appeler ceci avec lambdas ou std::function s ou quoi que vous ayez.