2017-09-07 1 views
3

Ici, je présente une première coupe de deux variantes de la fonction modèle over(vec, f).Sélection de modèle sur le type d'argument de l'objet fonction passé sans violer le principe DRY

Les deux versions parcourent un objet de type vectoriel et appellent un objet fonction pour chaque élément.

Une version appelle l'objet fonction avec deux arguments - une référence d'élément et un index - le second avec juste la référence d'élément.

L'idée est d'amener le compilateur à sélectionner la version qui correspond au lambda transmis, afin que l'utilisateur puisse exprimer l'intention dans la signature lambda sans avoir à sélectionner une fonction libre nommée différemment.

est ici le code:

#include <vector> 
#include <iostream> 

template<typename... Ts> struct make_void { typedef void type;}; 
template<typename... Ts> using void_t = typename make_void<Ts...>::type; 


template<class Vector, class F> 
auto over(Vector &&vec, F &&f) 
-> void_t<decltype(f(vec.operator[](std::declval<std::size_t>()), std::declval<std::size_t>()))> 
{ 
    const auto size = vec.size(); 
    for (std::size_t i = 0; i < size; ++i) { 
     f(vec[i], i); 
    } 
} 


template<class Vector, class F> 
auto over(Vector &&vec, F &&f) 
-> void_t<decltype(f(*vec.begin()))> 
{ 
    for (auto &&x : vec) { 
     f(x); 
    } 
} 

int main() { 
    std::vector<float> vf = {1.0, 1.1, 1.2}; 

    std::cout << "two-argument form:\n"; 
    over(vf, [](auto &&val, auto &&index) { 
     std::cout << index << " : " << val << std::endl; 
    }); 

    std::cout << "\none-argument form:\n"; 
    over(vf, [](auto &&val) { 
     std::cout << val << std::endl; 
    }); 
} 

Question:

Vous verrez que la clause à l'intérieur du générateur de type void_t<> de retour sait tout sur la mise en œuvre de la fonction. Je suis mécontent de ce que:

a) il fuit les détails de mise en œuvre dans l'interface et

b) il n'est pas DRY.

Y at-il une meilleure façon d'y parvenir qui:

a) permet la mise en œuvre de changer sans changer le modèle-facilitateur,

b) ne ressemble pas à mes chiens avait un jeu-combat sur mon clavier?

+1

'void_t ', il est même ne serait pas plus simple Résolvez votre problème DRY. – Jarod42

+0

@ Jarod42 c'est à peu près où j'ai commencé. Pour ce petit algorithme, ce n'est pas un gros problème, mais imaginez un 20 lignes. –

Répondre

1

En C++ 17, vous pouvez utiliser SFINAE basé sur std::is_invocable, quelque chose de similaire à:

template <class Vector, class F> 
std::enable_if_t<std::is_invocable<F, 
            typename Vector::value_type, 
            std::size_t>::value> 
over(const Vector& vec, F&& f) 
{ 
    const auto size = vec.size(); 
    for (std::size_t i = 0; i < size; ++i) { 
     f(vec[i], i); 
    } 
} 

template <class Vector, class F> 
std::enable_if_t<std::is_invocable<F, typename Vector::value_type>::value> 
over(const Vector& vec, F&& f) 
{ 
    const auto size = vec.size(); 
    for (const auto& e : vec) { 
     f(e); 
    } 
} 
+1

que se passe-t-il si le vecteur est const ou volatile? –

+0

Alors en effet, nous avons besoin d'un trait pour obtenir 'decltype (vec [0])' sans le répéter. (Mais de toute façon, l'écriture avec des traits ou 'decltype' est la répétition surtout avec la mise en œuvre :(). – Jarod42

2

Pour cet exemple, en évitant la « répétition » va être beaucoup plus de travail/complexité que la répétition lui-même, mais l'idée de base est de compter l'ar-iness de la fonction, et ensuite envoyer correctement. Un problème très similaire est discuté ici: Call function with part of variadic arguments. Utilisation de la mise en œuvre de function_traits vous pouvez peut mettre en œuvre une fonction appelée expédition (je l'ai appelé foo dans ma réponse à cette question):

template<typename F, std::size_t... Is, class Tup> 
void dispatch_impl(F && f, std::index_sequence<Is...>, Tup && tup) { 
    std::forward<F>(f)(std::get<Is>(std::move(tup))...); 
} 

template<typename F, typename... Args> 
void dispatch(F && f, Args&&... args) { 
    dispatch_impl(std::forward<F>(f), 
      std::make_index_sequence<function_traits<F>::arity>{}, 
      std::forward_as_tuple(args...)); 
} 


template<class Vector, class F> 
void over(Vector &&vec, F &&f) 
{ 
    std::size_t i = 0; 
    for (auto &&x : vec) { 
     dispatch(std::forward<F>(f), x, i); 
     ++i; 
    } 
} 

Cette réponse est 14 conforme aussi bien. Exemple en direct: http://coliru.stacked-crooked.com/a/14750cef6b735d7e. Edit: Cette approche ne fonctionne pas avec les lambdas génériques. Donc, une autre approche serait de mettre en œuvre cette répartition de la façon suivante:

template<typename F, typename T> 
auto dispatch(F && f, T && t, std::size_t i) -> decltype((std::forward<F>(f)(std::forward<T>(t)),0)) { 
    std::forward<F>(f)(std::forward<T>(t)); 
    return 0; 
} 

template<typename F, typename T> 
auto dispatch(F && f, T && t, std::size_t i) -> decltype((std::forward<F>(f)(std::forward<T>(t), i),0)) { 
    std::forward<F>(f)(std::forward<T>(t),i); 
    return 0; 
} 
+0

hmm. C'est à la fois bon et mauvais. D'une part, il est sec, mais de l'autre il faut une mise en œuvre commune. Je vais Asseyez-vous avec un peu –

+0

@RichardHodges Je suppose que je ne vois pas comment vous pouvez obtenir à la fois SEC et éviter de nécessiter une implémentation commune? Peut-être que cela aiderait si vous posté un exemple plus complexe (votre 20 doublure), donc nous On peut voir les parties qui sont vraiment communes et celles qui ne le sont pas Notons aussi: cette approche ne fonctionne pas avec les lambdas génériques Je préfère spécifier les types d'arguments à mes lambdas donc ça ne me dérange pas, mais je vois au dessus que tu as utilisé 'auto'. –

+0

Je seches dans le sens que nous ne devons pas répéter le fonctionnement interne de l'algorithme dans la chicanerie de enable_if (que vous avez accompli ici), mais au détriment de perdre la possibilité de changer les algorithmes en fonction le la fourni mbda (qui est dans mon exemple motivant). –

0

OK, voici ma première tentative sérieuse.

Y at-il quelque chose de mieux que cela?

#include <vector> 
#include <iostream> 
#include <string> 

namespace notstd 
{ 
    /* deduce the traits of a container argument, even if it's an rvalue-reference */ 
    template<class T> 
    struct container_traits 
    { 
     static_assert(not std::is_pointer<T>(), ""); 
     using without_reference_type = std::remove_reference_t<T>; 
     using base_type = std::remove_cv_t<without_reference_type>; 

     static constexpr auto is_const = std::is_const<without_reference_type>::value; 
     static constexpr auto is_volaile = std::is_volatile<without_reference_type>::value; 

     using base_value_type = typename base_type::value_type; 
     using value_type = std::conditional_t<is_const, std::add_const_t<base_value_type>, base_value_type>; 
    }; 

    template<class Function, class...Args> 
    struct is_compatible_function 
    { 
     template<class FArg> static auto test(FArg&& f) -> decltype(f(std::declval<Args>()...), void(), std::true_type()); 
     static auto test(...) -> decltype(std::false_type()); 

     static constexpr auto value = decltype(test(std::declval<Function>()))::value; 
    }; 
} 

/** 
* define the 2-argument algorithm, plus provide function compatibility checks 
*/ 
template<class Vector, class Function> 
struct over_op_2 
{ 
    using arg_1_type = std::add_lvalue_reference_t<typename notstd::container_traits<Vector>::value_type>; 
    using arg_2_type = std::size_t; 

    static constexpr auto is_compatible_function = notstd::is_compatible_function<Function, arg_1_type, arg_2_type>::value; 

    template<class VectorArg, class FunctionArg> 
    void operator()(VectorArg&& vec, FunctionArg&& f) const 
    { 
     std::size_t i = 0; 
     for (auto &&x : vec) { 
      f(x, i); 
      ++i; 
     } 
    } 
}; 

/** 
* define the 1-argument algorithm, plus provide function compatibility checks 
*/ 
template<class Vector, class Function> 
struct over_op_1 
{ 
    using arg_1_type = std::add_lvalue_reference_t<typename notstd::container_traits<Vector>::value_type>; 

    static constexpr auto is_compatible_function = notstd::is_compatible_function<Function, arg_1_type>::value; 

    template<class VectorArg, class FunctionArg> 
    void operator()(VectorArg&& vec, FunctionArg&& f) const 
    { 
     for (auto &&x : vec) { 
      f(x); 
     } 
    } 
}; 

/** 
* Choose op_2 if the Function type will allow it, otherwise op_1 if that's possible, otherwise void (error) 
*/ 
template<class Vector, class Function> 
struct select_over_op 
{ 
    using op_1 = over_op_1<Vector, Function>; 
    using op_2 = over_op_2<Vector, Function>; 
    using type = std::conditional_t 
    < 
     op_2::is_compatible_function, 
     op_2, 
     std::conditional_t 
     < 
      op_1::is_compatible_function, 
      op_1, 
      void 
     > 
    >; 

    static_assert(not std::is_same<type, void>(), "function signatures are incompatible"); 
                               ; 
}; 

/** 
* iterate over a vector-like container, calling f(elem, i) if available or f(elem) if not. 
* @param vec is a reference to a vector-like object 
* @param f is a function which is compatible with one of: 
*  void([const]value_type&, std::size_t), or 
*  void([const]value_type&) 
*/ 
template<class Vector, class F> 
decltype(auto) over(Vector &&vec, F &&f) 
{ 
    auto op = typename select_over_op<decltype(vec), decltype(f)>::type(); 
    return op(std::forward<Vector>(vec), std::forward<F>(f));  
} 



int main() { 
    std::vector<double> v{4.1,5.1,6.1}; 
    over(v, [] (auto x, auto y) { std::cout << x << ", " << y << "\n"; }); 
    over(v, [] (auto && x, auto&& y) { std::cout << x << ", " << y << "\n"; }); 
    over(v, [] (auto const& x, auto const& y) { std::cout << x << ", " << y << "\n"; }); 
    over(v, [] (auto x, auto&& y) { std::cout << x << ", " << y << "\n"; }); 
    over(v, [] (auto && x, auto&& y) { std::cout << x << ", " << y << "\n"; }); 
    over(v, [] (auto const& x, auto&& y) { std::cout << x << ", " << y << "\n"; }); 
    over(v, [] (auto x) { std::cout << x << "\n"; }); 
    over(v, [] (auto const& x) { std::cout << x << "\n"; }); 
    over(v, [] (auto && x) { std::cout << x << "\n"; }); 

    // converting to int ok (but meh) 
    over(v, [] (int x) { std::cerr << x << "\n"; }); 

    // converting to string correctly fails 
    // over(v, [] (std::string x) { std::cerr << x << "\n"; }); 

    // const vector... 
    const std::vector<double> vc{4.1,5.1,6.1}; 
    over(vc, [] (auto && x, auto&& y) { std::cout << x << ", " << y << "\n"; }); 

    // breaking const contract on the value_type also fails 
    // over(vc, [] (double& x, auto&& y) { std::cout << x << ", " << y << "\n"; }); 

    return 0; 
} 

http://coliru.stacked-crooked.com/a/cab94488736b75ed