Équivalent C ++ de StringBuffer / StringBuilder?

184

Y at - il une classe modèle standard C ++ Library qui fournit des fonctionnalités de concaténation de chaîne efficace, similaire à C # 's StringBuilder ou Java StringBuffer ?

An̲̳̳drew
la source
3
la réponse courte est: Oui, STL a une classe pour cela et c'est le cas std::ostringstream.
CoffeDeveloper
Salut @andrew. Pouvez-vous changer la réponse acceptée? Il y a une réponse gagnante claire et ce n'est pas la réponse actuellement acceptée.
null

Réponses:

53

REMARQUE cette réponse a récemment reçu une certaine attention. Je ne préconise pas cela comme une solution (c'est une solution que j'ai vue dans le passé, avant le TSL). Il est une approche intéressante et ne doit être appliqué sur std::stringou std::stringstreamsi , après votre code de profilage vous découvrez ce fait une amélioration.

J'utilise normalement soit std::stringou std::stringstream. Je n'ai jamais eu de problèmes avec ces derniers. Je réserverais normalement de la place d'abord si je connais à l'avance la taille approximative de la corde.

J'ai vu d'autres personnes fabriquer leur propre constructeur de cordes optimisé dans un passé lointain.

class StringBuilder {
private:
    std::string main;
    std::string scratch;

    const std::string::size_type ScratchSize = 1024;  // or some other arbitrary number

public:
    StringBuilder & append(const std::string & str) {
        scratch.append(str);
        if (scratch.size() > ScratchSize) {
            main.append(scratch);
            scratch.resize(0);
        }
        return *this;
    }

    const std::string & str() {
        if (scratch.size() > 0) {
            main.append(scratch);
            scratch.resize(0);
        }
        return main;
    }
};

Il utilise deux chaînes, l'une pour la majorité de la chaîne et l'autre comme zone de scratch pour la concaténation de chaînes courtes. Il optimise les ajouts en regroupant les opérations d'ajout courtes dans une petite chaîne, puis en les ajoutant à la chaîne principale, réduisant ainsi le nombre de réallocations requises sur la chaîne principale à mesure qu'elle s'agrandit.

Je n'ai pas eu besoin de cette astuce avec std::stringou std::stringstream. Je pense qu'il a été utilisé avec une bibliothèque de chaînes tierce avant std :: string, c'était il y a si longtemps. Si vous adoptez une stratégie comme ce profil, votre candidature en premier.

iain
la source
13
Réinventer la roue. std :: stringstream est la bonne réponse. Voir les bonnes réponses ci-dessous.
Kobor42
13
@ Kobor42 Je suis d'accord avec vous comme je le souligne sur la première et la dernière ligne de ma réponse.
iain
1
Je ne pense pas que la scratchcorde accomplisse vraiment quoi que ce soit ici. Le nombre de réallocations de la chaîne principale dépendra en grande partie de sa taille finale et non du nombre d'opérations d'ajout, à moins que l' stringimplémentation ne soit vraiment médiocre (c'est-à-dire qu'elle n'utilise pas de croissance exponentielle). Donc, «grouper» le appendn'aide pas parce qu'une fois que le sous string- jacent est grand, il ne se développera qu'occasionnellement de toute façon. En plus de cela, il ajoute un tas d'opérations de copie redondantes et peut plus de réallocations (d'où des appels à new/ delete) puisque vous ajoutez à une chaîne courte.
BeeOnRope
@BeeOnRope Je suis d'accord avec vous.
iain
Je suis presque sûr str.reserve(1024);que ce serait plus rapide que cette chose
hanshenrik
160

La manière C ++ serait d'utiliser std :: stringstream ou simplement des concaténations de chaînes simples. Les chaînes C ++ sont modifiables, donc les considérations de performances de la concaténation sont moins préoccupantes.

en ce qui concerne le formatage, vous pouvez faire tout le même formatage sur un flux, mais d'une manière différente, similaire àcout . ou vous pouvez utiliser un foncteur fortement typé qui encapsule ceci et fournit une interface de type String.Format, par exemple boost :: format

jk.
la source
59
Les chaînes C ++ sont mutables : exactement. La raison entière StringBuilderexiste est de couvrir l'inefficacité du type String de base immuable de Java . En d'autres termes, StringBuilderc'est du patchwork, nous devrions donc être heureux de ne pas avoir besoin d'une telle classe en C ++.
bobobobo
57
Les chaînes immuables @bobobobo ont cependant d'autres avantages, ses chevaux pour les cours
jk.
8
Les concaténations de chaînes simples ne créent-elles pas un nouvel objet, donc le même problème qu'avec l'immuabilité en Java? Considérez que toutes les variables sont des chaînes dans l'exemple suivant: a = b + c + d + e + f; Ne va-t-il pas appeler l'opérateur + sur b et c, puis l'opérateur + sur le résultat et d, etc.?
Serge Rogatch
9
Attendez une minute les gens, la classe de chaînes standard sait comment muter elle-même mais cela ne signifie pas que l'inefficacité n'est pas là. Autant que je sache, std :: string ne peut pas simplement étendre la taille de son caractère interne *. Cela signifie que le muter d'une manière qui nécessite plus de caractères nécessite une réallocation et une copie. Ce n'est pas différent d'un vecteur de caractères et il est certainement préférable de réserver l'espace dont vous avez besoin dans ce cas.
Trygve Skogsholm
7
@TrygveSkogsholm - ce n'est pas différent d'un vecteur de caractères, mais bien sûr, la "capacité" de la chaîne peut être plus grande que sa taille, donc tous les ajouts n'ont pas besoin d'une réallocation. En général, les chaînes utiliseront une stratégie de croissance exponentielle, de sorte que l'ajout d'amortit toujours à une opération de coût linéaire. C'est différent des chaînes immuables de Java dans lesquelles chaque opération d'ajout doit copier tous les caractères des deux chaînes dans une nouvelle, de sorte qu'une série d'ajouts se termine comme O(n)en général.
BeeOnRope
93

La std::string.appendfonction n'est pas une bonne option car elle n'accepte pas de nombreuses formes de données. Une alternative plus utile consiste à utiliser std::stringstream; ainsi:

#include <sstream>
// ...

std::stringstream ss;

//put arbitrary formatted data into the stream
ss << 4.5 << ", " << 4 << " whatever";

//convert the stream buffer into a string
std::string str = ss.str();
Stu
la source
43

std::string est l'équivalent C ++: il est mutable.

dan04
la source
13

Vous pouvez utiliser .append () pour simplement concaténer des chaînes.

std::string s = "string1";
s.append("string2");

Je pense que vous pourriez même être en mesure de faire:

std::string s = "string1";
s += "string2";

En ce qui concerne les opérations de formatage des C # StringBuilder, je crois snprintf(ou sprintfsi vous voulez risquer d'écrire du code bogué ;-)) dans un tableau de caractères et de reconvertir en chaîne est à peu près la seule option.

Andy Shellam
la source
Pas de la même manière que printf ou String.Format de .NET, n'est-ce pas?
Andy Shellam
1
c'est un peu malhonnête de dire qu'ils sont le seul moyen
jk.
2
@jk - ils sont le seul moyen de comparer la capacité de formatage de StringBuilder de .NET, ce que la question originale posait spécifiquement. J'ai dit "je crois" donc je peux me tromper, mais pouvez-vous me montrer un moyen d'obtenir les fonctionnalités de StringBuilder en C ++ sans utiliser printf?
Andy Shellam
mis à jour ma réponse pour inclure des options de formatage alternatives
jk.
6

Étant donné que std::stringC ++ est modifiable, vous pouvez l'utiliser. Il a += operatorune appendfonction et une fonction.

Si vous devez ajouter des données numériques, utilisez les std::to_stringfonctions.

Si vous voulez encore plus de flexibilité sous la forme de pouvoir sérialiser n'importe quel objet en une chaîne, utilisez la std::stringstreamclasse. Mais vous devrez implémenter vos propres fonctions d'opérateur de streaming pour qu'il fonctionne avec vos propres classes personnalisées.

Daemin
la source
4

std :: string's + = ne fonctionne pas avec const char * (ce que des trucs comme "string to add" semblent être), donc l'utilisation de stringstream est la plus proche de ce qui est requis - vous utilisez simplement << au lieu de +

Sergeys
la source
3

Un générateur de chaînes pratique pour C ++

Comme beaucoup de gens ont déjà répondu, std :: stringstream est la méthode de choix. Cela fonctionne bien et dispose de nombreuses options de conversion et de formatage. IMO, il a cependant un défaut assez gênant: vous ne pouvez pas l'utiliser comme une seule ligne ou comme une expression. Il faut toujours écrire:

std::stringstream ss;
ss << "my data " << 42;
std::string myString( ss.str() );

ce qui est assez ennuyeux, surtout lorsque vous souhaitez initialiser des chaînes dans le constructeur.

La raison en est que a) std :: stringstream n'a pas d'opérateur de conversion en std :: string et b) les opérateurs << () du stringstream ne renvoient pas une référence stringstream, mais une référence std :: ostream à la place - qui ne peut pas être davantage calculé comme un flux de chaînes.

La solution est de surcharger std :: stringstream et de lui donner de meilleurs opérateurs correspondants:

namespace NsStringBuilder {
template<typename T> class basic_stringstream : public std::basic_stringstream<T>
{
public:
    basic_stringstream() {}

    operator const std::basic_string<T> () const                                { return std::basic_stringstream<T>::str();                     }
    basic_stringstream<T>& operator<<   (bool _val)                             { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (char _val)                             { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (signed char _val)                      { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (unsigned char _val)                    { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (short _val)                            { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (unsigned short _val)                   { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (int _val)                              { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (unsigned int _val)                     { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (long _val)                             { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (unsigned long _val)                    { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (long long _val)                        { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (unsigned long long _val)               { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (float _val)                            { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (double _val)                           { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (long double _val)                      { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (void* _val)                            { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (std::streambuf* _val)                  { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (std::ostream& (*_val)(std::ostream&))  { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (std::ios& (*_val)(std::ios&))          { std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (std::ios_base& (*_val)(std::ios_base&)){ std::basic_stringstream<T>::operator << (_val); return *this; }
    basic_stringstream<T>& operator<<   (const T* _val)                         { return static_cast<basic_stringstream<T>&>(std::operator << (*this,_val)); }
    basic_stringstream<T>& operator<<   (const std::basic_string<T>& _val)      { return static_cast<basic_stringstream<T>&>(std::operator << (*this,_val.c_str())); }
};

typedef basic_stringstream<char>        stringstream;
typedef basic_stringstream<wchar_t>     wstringstream;
}

Avec cela, vous pouvez écrire des choses comme

std::string myString( NsStringBuilder::stringstream() << "my data " << 42 )

même dans le constructeur.

Je dois avouer que je n'ai pas mesuré les performances, car je ne l'ai pas encore utilisé dans un environnement qui utilise beaucoup la construction de chaînes, mais je suppose que ce ne sera pas bien pire que std :: stringstream, puisque tout est fait via des références (sauf la conversion en chaîne, mais c'est également une opération de copie dans std :: stringstream)

user2328447
la source
C'est chouette. Je ne vois pas pourquoi std::stringstreamne se comporte pas de cette façon.
einpoklum
1

Le conteneur Rope peut valoir la peine si vous devez insérer / supprimer une chaîne dans l'emplacement aléatoire de la chaîne de destination ou pour une longue séquence de caractères. Voici un exemple de l'implémentation de SGI:

crope r(1000000, 'x');          // crope is rope<char>. wrope is rope<wchar_t>
                                // Builds a rope containing a million 'x's.
                                // Takes much less than a MB, since the
                                // different pieces are shared.
crope r2 = r + "abc" + r;       // concatenation; takes on the order of 100s
                                // of machine instructions; fast
crope r3 = r2.substr(1000000, 3);       // yields "abc"; fast.
crope r4 = r2.substr(1000000, 1000000); // also fast.
reverse(r2.mutable_begin(), r2.mutable_end());
                                // correct, but slow; may take a
                                // minute or more.
Igor
la source
0

Je voulais ajouter quelque chose de nouveau pour les raisons suivantes:

Lors d'une première tentative, je n'ai pas réussi à battre

std::ostringstream de operator<<

efficacité, mais avec plus d'essais j'ai pu faire un StringBuilder plus rapide dans certains cas.

Chaque fois que j'ajoute une chaîne, je stocke simplement une référence quelque part et augmente le compteur de la taille totale.

La vraie façon dont je l'ai finalement implémenté (Horreur!) Est d'utiliser un tampon opaque (std :: vector <char>):

  • En-tête de 1 octet (2 bits pour indiquer si les données suivantes sont: chaîne, chaîne ou octet déplacée [])
  • 6 bits pour indiquer la longueur de l'octet []

pour l'octet []

  • Je stocke directement des octets de chaînes courtes (pour un accès mémoire séquentiel)

pour les chaînes déplacées (chaînes ajoutées std::move)

  • Le pointeur vers un std::stringobjet (nous avons la propriété)
  • définir un indicateur dans la classe s'il y a des octets réservés inutilisés

pour les cordes

  • Le pointeur vers un std::stringobjet (pas de propriété)

Il y a aussi une petite optimisation, si la dernière chaîne insérée a été déplacée, elle vérifie les octets libres réservés mais inutilisés et y stocke d'autres octets au lieu d'utiliser le tampon opaque (c'est pour économiser de la mémoire, cela le rend en fait légèrement plus lent , dépend peut-être aussi du CPU, et il est rare de voir de toute façon des chaînes avec un espace réservé supplémentaire)

C'était finalement un peu plus rapide que std::ostringstreammais cela a quelques inconvénients:

  • J'ai supposé des types de caractères de longueur fixe (donc 1,2 ou 4 octets, pas bons pour UTF8), je ne dis pas que cela ne fonctionnera pas pour UTF8, je ne l'ai pas vérifié pour la paresse.
  • J'ai utilisé une mauvaise pratique de codage (tampon opaque, facile à rendre non portable, je crois que le mien est portable d'ailleurs)
  • Manque toutes les fonctionnalités de ostringstream
  • Si une chaîne référencée est supprimée avant la fusion de toutes les chaînes: comportement indéfini.

conclusion? utilisation std::ostringstream

Il corrige déjà le plus gros goulot d'étranglement tout en gagnant quelques points de vitesse avec la mise en œuvre de la mine ne vaut pas les inconvénients.

CoffeDeveloper
la source