J'arrive au C ++ depuis et où il existe des moyens intégrés de convertir des types de données en chaînes .

Par exemple, dans Haskell il y a la fonction polymorphe show.

Je suis intéressé par la création de fonctions de modèle en C ++ qui feraient quelque chose de similaire.

Par exemple, nous pourrions convertir vector<int> en une chaîne quelque chose comme ça.

string toString(vector<int> v)
{
    ostringstream o;
    for (int elem: v)
        o << elem << " ";
    return o.str()
}

Ceci met une représentation sous forme de chaîne des int s tous sur une ligne. Maintenant, que faire si je voulais convertir un vector<vector<int> > de cette manière.

string toString(vector<vector<int> > v)
{
   ostringstream o;
   for (auto elem : v)
   {
      o << toString(elem) << "\n";
   }
}

Ma question est : et si je voulais créer un toString polymorphe qui fonctionne avec vector<class A> et vector<vector<class A>? Comment pourrais-je procéder?

J'aurais besoin d'ajouter des fonctionnalités pour convertir le type class A en std::string: est-ce que je ne fournis qu'au moins une spécialisation de toString pour ce type? Le mécanisme de modèle règle-t-il tout cela?

Ou existe-t-il déjà un code pour le faire?

3
composerMike 29 août 2020 à 04:51

2 réponses

Meilleure réponse

Et si je voulais créer un toString polymorphe qui fonctionne avec vector<class A> et vector<vector<class A>? Comment pourrais-je procéder?

Oui, c'est possible dans , par la cobination de {{X0 }} et un modèle de fonction récursive (c'est-à-dire faire de toString comme modèle de fonction récursive).

Avant de sauter dans le modèle de fonction générique, votre class A doit implémenter une surcharge de operator<< pour que std::ostringstream::operator<< peut en profiter. Par exemple, considérons

struct A
{
   char mChar;
   // provide a overload for operator<< for the class!
   friend std::ostream& operator<<(std::ostream& out, const A& obj) /* noexcept */ {
      return out << obj.mChar;
   }
};

Maintenant, la fonction toString ressemblerait à quelque chose comme suit:

#include <type_traits> // std::is_floating_point_v, std::is_integral_v, std::is_same_v
                       // std::remove_const_t, std::remove_reference_t

template<typename Type>
inline static constexpr bool isAllowedType = std::is_floating_point_v<Type>
|| std::is_integral_v<Type> 
|| std::is_same_v<A, Type>;
//^^^^^^^^^^^^^^^^^^^ --> struct A has been added to the
//                        allowed types(i.e types who has operator<< given)

template<typename Vector>
std::string toString(const Vector& vec) /* noexcept */
{
   std::ostringstream stream; 
   // value type of the passed `std::vector<Type>`
   using ValueType = std::remove_const_t< 
      std::remove_reference_t<decltype(*vec.cbegin())>
   >;
   // if it is allowed type do  concatenation!
   if constexpr (isAllowedType<ValueType>) 
   {
      for (const ValueType& elem : vec)
         stream << elem << " ";
 
      stream << '\n';
      return stream.str();
   }
   else
   {
      // otherwise do the recursive call to toString
      // for each element of passed vec
      std::string result;
      for (const ValueType& innerVec : vec)
         result += toString(innerVec);

      return result; // return the concatenated string
   }   
}

Vous pouvez maintenant appeler le toString au std::vector<std::vector<A>> ainsi qu'au std::vector<A> aObjs, ainsi qu'au std::vector< /* primitive types */ >.

(Voir la démo complète en ligne en direct)


Dois-je simplement fournir au moins une spécialisation de toString pour ce type? Le mécanisme de modèle règle-t-il tout cela?

La spécialisation des modèles est également une autre option. Cependant, si vous avez accès à C ++ 17, je suggère la manière ci-dessus, qui triera tous les types que vous avez fournis dans la question.

3
JeJo 29 août 2020 à 08:30

Il n'existe actuellement aucun moyen générique direct de le faire mais vous pouvez simplement créer le vôtre. Voici un exemple de programme qui imitera le comportement que vous recherchez.

#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

template<typename T>
std::string toString(const std::vector<T>& vec) {
    std::ostringstream stream;
    for (auto& elem : vec) {
        stream << elem << " ";
    }
    stream << '\n';
    return stream.str();
}

template<typename T>
std::string toString(const std::vector<std::vector<T>>& vec) {
    std::ostringstream stream;
    for (auto& elem : vec) {
        stream << toString(elem);
    }
    stream << '\n';
    return stream.str();
}


int main() {
    try {
        std::vector<int> valuesA{ 1, 2, 3, 4 };
        std::cout << toString(valuesA) << '\n';

        std::vector<std::vector<float>> valuesB { {1.0f, 2.0f, 3.0f},
                                                  {4.0f, 5.0f, 6.0f},
                                                  {7.0f, 8.0f, 9.0f}
                                                };
        std::cout << toString(valuesB) << '\n';
    } catch( const std::exception& e ) {
        std::cerr << "Exception Thrown: " << e.what() << std::endl;
        return EXIT_FAILURE;
    } catch( ... ) {
        std::cerr << __FUNCTION__ << " Caught Unknown Exception" << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Sortie

1 2 3 4

1 2 3
4 5 6
7 8 9

Le code ci-dessus fonctionnera pour vector<T> et vector<vector<T>> mais il ne fonctionnera pas dans toutes les situations. Si vous avez un vecteur imbriqué dans un vecteur, la déclaration de fonction ne le reconnaîtra pas. De plus, il ne reconnaîtra pas d'autres conteneurs tels que maps, sets, lists, queues, etc ... à partir de là, vous devrez alors générer cette fonction pour accepter tous les différents types de conteneurs ...

À ce stade, vous commencerez à voir la duplication de code et les modèles répétitifs. Donc, au lieu de déclarer la fonction comme:

template<T>
std::string toString(const std::vector<T>& vec) { /* ... */ }

Vous pouvez modéliser le container lui-même ...

template<template<class> class Container, class Ty>
std::string toString(const Container<Ty>& container ) { /*... */ }

Maintenant, cela fonctionnera pour la plupart des conteneurs, mais pour certains conteneurs, il peut être un peu difficile de le faire fonctionner correctement, comme std::map car il peut prendre des valeurs d'un std::pair, ou il peut prendre deux types correspondants basé sur sa déclaration en conjonction avec ses constructeurs qui utilisent l'initialisation d'accolades. C'est là que vous devrez peut-être surcharger la fonction pour ce conteneur spécifique, mais l'idée générale s'applique toujours.

C'est plus que simplement utiliser templates, c'est aussi utiliser templates où leurs arguments sont templates eux-mêmes et si vous ne les connaissez pas, leur syntaxe peut être un peu intimidante pour un débutant. Je suis sûr que vous pouvez trouver de nombreuses recherches sur les paramètres template template ...


Modifier

En passant, vous devez toujours faire attention à ce que type soit passé dans Container<Ty>. Pour les types intégrés simples tels que int, float, char, double, etc., c'est simple ...

Cependant, que se passe-t-il si vous avez votre propre class ou struct défini par l'utilisateur ...

class Foo {
private:
    int bar;
    float baz;
public:
    Foo() : bar{0}, baz{0.0f} {}
    Foo(int barIn, float bazIn) : bar{barIn}, baz{bazIn} {}
};

Ensuite, vous ou quelqu'un d'autre qui essaie d'utiliser votre code décide de faire:

std::vector<Foo> foos { Foo(1, 3.5f), Foo(2, 4.0f), Foo(3, 3.14159f) };
std::string report = toString(foos);

Ce qui précède n'est pas si simple car le programme ou les fonctions ne sauront pas comment convertir Foo en std::string. Il faut donc tenir compte de la prudence et de la considération. C'est là que vous pourriez avoir besoin de fonctions d'aide supplémentaires pour convertir des classes ou des structures définies par l'utilisateur en std::string, alors vous devrez spécialiser votre fonction toString() pour ces types et utiliser la fonction d'aide à la conversion en son sein ...


Maintenant, au fur et à mesure que le langage C++ évolue avec chaque version du standard et les améliorations apportées à divers compilateurs, les choses ont tendance à se simplifier, cela étant dit, cela deviendra bientôt une occurrence courante et un modèle répétitif commun qui pourrait éventuellement devenir rationalisé. Il y a des perspectives positives pour l'avenir de C++. Il existe déjà des outils pour vous aider à créer le vôtre. Au fil du temps, ces outils deviennent facilement accessibles à utiliser et peuvent même simplifier votre code et votre temps de production.

3
Francis Cugler 29 août 2020 à 09:27