La fonction std :: transform-like qui renvoie un conteneur transformé

J’essaie d’implémenter une fonction similaire à l’algorithme std::transform mais au lieu de prendre l’iterator de sortie par un argument, je veux créer et renvoyer un conteneur avec des éléments d’entrée transformés.

Disons qu’il s’appelle transform_container et prend deux arguments: container et functor. Il devrait renvoyer le même type de conteneur mais éventuellement paramétré par un type d’élément différent (le Functor peut renvoyer un élément de type différent).

Je voudrais utiliser ma fonction comme dans l’exemple ci-dessous:

 std::vector vi{ 1, 2, 3, 4, 5 }; auto vs = transform_container(vi, [] (int i) { return std::to_ssortingng(i); }); //vs will be std::vector assert(vs == std::vector({"1", "2", "3", "4", "5"})); std::set si{ 5, 10, 15 }; auto sd = transform_container(si, [] (int i) { return i / 2.; }); //sd will be of type std::set assert(sd == std::set({5/2., 10/2., 15/2.})); 

J’ai pu écrire deux fonctions – une pour std::set et une pour std::vector – qui semblent fonctionner correctement. Ils sont identiques, à l’exception du nom de conteneur. Leur code est listé ci-dessous.

 template auto transform_container(const std::vector &v, Functor &&f) -> std::vector { std::vector ret; std::transform(std::begin(v), std::end(v), std::inserter(ret, ret.end()), f); return ret; } template auto transform_container(const std::set &v, Functor &&f) -> std::set { std::set ret; std::transform(std::begin(v), std::end(v), std::inserter(ret, ret.end()), f); return ret; } 

Cependant, lorsque j’ai tenté de les fusionner en une seule fonction générale compatible avec tous les conteneurs, j’ai rencontré de nombreux problèmes. L’ set et le vector sont des modèles de classe, donc mon modèle de fonction doit prendre un paramètre de modèle de modèle. De plus, les modèles de set et de vecteur ont un nombre différent de parameters de type qui doivent être correctement ajustés.

Quelle est la meilleure façon de généraliser les deux modèles de fonctions ci-dessus en une fonction compatible avec tout type de conteneur compatible?

Cas les plus simples: types de conteneurs correspondants

Pour le cas simple où le type d’entrée correspond au type de sortie (ce dont je suis convaincu que ce n’est pas ce que vous demandez), passez à un niveau supérieur. Au lieu de spécifier le type T que votre conteneur utilise et d’essayer de se spécialiser sur un vector , etc., spécifiez simplement le type du conteneur lui-même:

 template  Container transform_container(const Container& c, Functor &&f) { Container ret; std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f); return ret; } 

Plus de complexité: types de valeurs compatibles

Puisque vous souhaitez essayer de modifier le type d’élément stocké par le conteneur, vous devez utiliser un paramètre de modèle de modèle et modifier le T utilisé par le conteneur renvoyé.

 template < template  class Container, typename Functor, typename T, // <-- This is the one we'll override in the return container typename U = std::result_of::type, typename... Ts > Container transform_container(const Container& c, Functor &&f) { Container ret; std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f); return ret; } 

Qu’en est-il des types de valeurs incompatibles?

Cela ne fait que nous y rendre. Il fonctionne très bien avec une transformation de signed en unsigned mais quand elle est résolue avec T=int et S=std::ssortingng et en manipulant des ensembles, elle essaye d’instancier std::set, ...> et donc ne comstack pas.

Pour corriger cela, nous voulons prendre un ensemble arbitraire de parameters et remplacer les instances de T par U , même si ce sont les parameters d’autres parameters de modèle. Ainsi, std::set> devrait devenir std::set> , etc. Cela implique une méta-programmation personnalisée, comme suggéré par d’autres réponses.

Métaprogrammation de modèles à la rescousse

Créons un template, replace_type -le replace_type et le convertissons en U , et K en K . Commençons par traiter le cas général. Si ce n’est pas un type basé sur un modèle, et qu’il ne correspond pas à T , son type doit restr K :

 template  struct replace_type { using type = K; }; 

Puis une spécialisation. Si ce n’est pas un type basé sur un modèle, et qu’il correspond à T , son type deviendra U :

 template  struct replace_type { using type = U; }; 

Et enfin une étape récursive pour gérer les parameters pour les types basés sur des modèles. Pour chaque type dans les parameters d’un type basé sur un modèle, remplacez les types en conséquence:

 template  

Et enfin mettre à jour transform_container pour utiliser replace_type :

 template < template  class Container, typename Functor, typename T, typename U = typename std::result_of::type, typename... Ts, typename Result = typename replace_type, T, U>::type > Result transform_container(const Container& c, Functor &&f) { Result ret; std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f); return ret; } 

Est-ce complet?

Le problème avec cette approche est que ce n’est pas nécessairement sûr. Si vous convertissez de Container en Container , c’est probablement correct. Mais lors de la conversion de Container en Container il est plausible qu’un autre paramètre de template ne soit pas converti du type builtin_type en SomethingElse . De plus, des conteneurs alternatifs tels que std::map ou std::array apportent plus de problèmes à la partie.

La gestion de std::map et std::unordered_map n’est pas trop grave. Le principal problème est que replace_type doit remplacer plus de types. Non seulement existe-t-il un remplacement T -> U , mais également un remplacement std::pair -> std::pair . Cela augmente le niveau de préoccupation pour les remplacements de type indésirables car il y a plus d’un seul type en vol. Cela dit, voici ce que j’ai trouvé pour travailler; notez qu’en test j’ai dû spécifier le type de retour de la fonction lambda qui a transformé les paires de ma carte:

 // map-like classes are harder. You have to replace both the key and the key-value pair types // Give a base case replacing a pair type to resolve ambiguities introduced below template  struct replace_type, std::pair, std::pair> { using type = std::pair; }; // Now the extended case that replaces T1->U1 and pair -> pair template  

Qu’en est-il de std :: array?

La gestion de std::array ajoute à la douleur, car ses parameters de modèle ne peuvent pas être déduits dans le modèle ci-dessus. Comme Jarod42 le note, cela est dû à ses parameters, y compris les valeurs, au lieu de simplement les types. Je suis parti en ajoutant des spécialisations et en introduisant un helper contained_type qui extrait T pour moi (remarque, par constructeur, il est préférable d’écrire le nom de typename Container::value_type et fonctionne pour tous les types dont j’ai parlé). Même sans les spécialisations std::array cela me permet de simplifier mon modèle transform_container à la suivante (cela peut être une victoire même sans la prise en charge de std::array ):

 template  struct replace_type, T, U> { using type = std::array; }; // contained_type::type is T when C is vector, set, or std::array. // This is better written as typename C::value_type, but may be necessary for bad containers template  struct contained_type { }; template  

Cependant, l’implémentation actuelle de transform_container utilise std::inserter qui ne fonctionne pas avec std::array . Bien qu’il soit possible de faire plus de spécialisations, je vais laisser cela comme un exercice de soupe modèle pour un lecteur intéressé. Je choisirais personnellement de vivre sans support pour std::array dans la plupart des cas.

Afficher l’exemple en direct cumulatif


Divulgation complète: bien que cette approche ait été influencée par la citation par Ali de la réponse de Kerrek SB, je n’ai pas réussi à le faire fonctionner dans Visual Studio 2013, j’ai donc construit moi-même l’alternative ci-dessus. De nombreux remerciements à certaines parties de la réponse originale de Kerrek SB sont toujours nécessaires, ainsi que des encouragements et des encouragements de Constructor et Jarod42.

Quelques remarques

La méthode suivante permet de transformer des conteneurs de tout type à partir de la bibliothèque standard (il y a un problème avec std::array , voir ci-dessous). La seule exigence pour le conteneur est qu’il doit utiliser les classes std::equal_to , std::less , std::equal_to et std::hash par défaut. Nous avons donc 3 groupes de conteneurs de la bibliothèque standard:

  1. Conteneurs avec un paramètre de type de modèle autre que celui par défaut (type de valeur):

    • std::vector , std::deque , std::list , std::forward_list , [ std::valarray ]
    • std::queue , std::priority_queue , std::stack
    • std::set , std::unordered_set
  2. Conteneurs avec deux parameters de type de modèle autres que ceux par défaut (type de clé et type de valeur):

    • std::map , std::multi_map , std::multi_map , std::multi_map
  3. Conteneur avec deux parameters non définis par défaut: paramètre de type (type de valeur) et paramètre non-type (taille):

    • std::array

la mise en oeuvre

convert_container classe auxiliaire convert_container convertit les types de type de conteneur d’entrée connus ( InputContainer ) et le type de valeur de sortie ( OutputType ) dans le type du conteneur de sortie ( typename convert_container::type ):

 template  struct convert_container; // conversion for the first group of standard containers template  

transform_container fonction transform_container :

 template < class InputContainer, class Functor, class InputType = typename InputContainer::value_type, class OutputType = typename std::result_of::type, class OutputContainer = convert_container_t > OutputContainer transform_container(const InputContainer& ic, Functor f) { OutputContainer oc; std::transform(std::begin(ic), std::end(ic), std::inserter(oc, oc.end()), f); return oc; } 

Exemple d’utilisation

Voir l’ exemple en direct avec les conversions suivantes:

  • std::vector -> std::vector ,
  • std::set -> std::set ,
  • std::map -> std::map .

Problèmes

std::array -> std::array conversion std::array -> std::array ne comstack pas car std::array n’a pas insert méthode nécessaire à cause de std::inserter ). transform_container fonction transform_container ne devrait pas fonctionner pour cette raison avec les conteneurs suivants: std::forward_list , std::queue , std::priority_queue , std::stack , [ std::valarray ].

Faire cela en général sera très difficile.

Tout d’abord, considérons std::vector> , et disons que votre foncteur transforme T->U Non seulement nous devons mapper l’argument du premier type, mais nous devons vraiment utiliser Allocator::rebind pour obtenir le second. Cela signifie que nous avons besoin de savoir que le second argument est un allocateur en premier lieu … ou que nous avons besoin de machines pour vérifier qu’il a un rebind membre de rebind et qu’il l’utilise.

Ensuite, considérez std::array . Ici, nous devons savoir que le deuxième argument doit être copié littéralement dans notre std::array . Peut-être pouvons-nous prendre des parameters sans type sans les modifier, relier les parameters de type qui ont un modèle de membre de rebind et remplacer le littéral T par U ?

Maintenant, std::map, Allocator=std::allocator>> . Nous devons prendre Key sans changement, remplacer T par U , prendre Compare sans modification et relier Allocator à std::allocator> . C’est un peu plus compliqué.

Alors, pouvez-vous vivre sans cette flexibilité? Êtes-vous heureux d’ignorer les conteneurs associatifs et de supposer que l’allocateur par défaut convient à votre conteneur de sortie transformé?

La difficulté majeure est de récupérer le type de Container from Conainer . J’ai dérobé sans vergogne le code de la métaprogrammation de modèles: (trait pour?) Disséquant un modèle spécifié en types T , en particulier, la réponse de Kerrek SB (la réponse acceptée), comme je suis pas familier avec la métaprogrammation de modèles.

 #include  #include  #include  // stolen from Kerrek SB's answer template  struct tmpl_rebind { typedef T type; }; template  

J’ai testé ce code avec gcc 4.7.2 et clang 3.5 et fonctionne comme prévu.

Comme le fait remarquer Yakk , ce code comporte quelques réserves: “… votre ré-édition devrait remplacer tous les arguments, ou seulement le premier? Incertain. Devrait-il remplacer récursivement T0 par T1 dans les arguments suivants? Ie std::map> -> std::map> ? “ Je vois aussi des pièges avec le code ci-dessus (par exemple, comment traiter avec différents allocateurs, voir aussi la réponse inutile ).

Néanmoins, je pense que le code ci-dessus est déjà utile pour les cas d’utilisation simples. Si nous écrivions une fonction d’utilité à soumettre à un renforcement, je serais plus motivé pour étudier ces problèmes plus avant. Mais il y a déjà une réponse acceptée, donc je considère que l’affaire est close.


Un grand merci à Constructor, Dyp et Yakk pour avoir souligné mes erreurs / occasions manquées d’amélioration.