C ++ equivalente de StringBuffer / StringBuilder?

184

¿Hay una clase de C ++ Biblioteca de plantillas estándar que proporciona la funcionalidad de concatenación de cadenas eficiente, similar a C # 's StringBuilder o de Java StringBuffer ?

Andrés
fuente
3
la respuesta corta es: Sí, STL tiene una clase para eso y lo es std::ostringstream.
CoffeDeveloper
Hola @ Andrew. ¿Puedes cambiar la respuesta aceptada? Hay una respuesta ganadora clara y no es la respuesta aceptada actual.
nulo

Respuestas:

53

NOTA: esta respuesta ha recibido alguna atención recientemente. No estoy abogando por esto como una solución (es una solución que he visto en el pasado, antes de la STL). Es un enfoque interesante y solo debe aplicarse sobre std::stringostd::stringstream si después de perfilar su código descubre que esto mejora.

Normalmente uso cualquiera std::stringostd::stringstream . Nunca he tenido ningún problema con estos. Normalmente reservaría algo de espacio primero si supiera de antemano el tamaño aproximado de la cuerda.

He visto a otras personas hacer su propio generador de cadenas optimizado en el pasado distante.

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

Utiliza dos cadenas, una para la mayoría de la cadena y la otra como un área de scratch para concatenar cadenas cortas. Optimiza los agregados al agrupar las operaciones de agregado corto en una cadena pequeña y luego agregar esto a la cadena principal, reduciendo así el número de reasignaciones requeridas en la cadena principal a medida que se hace más grande.

No he requerido este truco con std::stringo std::stringstream. Creo que se usó con una biblioteca de cadenas de terceros antes de std :: string, fue hace mucho tiempo. Si adopta una estrategia como este perfil, su aplicación primero.

iain
fuente
13
Reinventando la rueda. std :: stringstream es la respuesta correcta. Ver buenas respuestas a continuación.
Kobor42
13
@ Kobor42 Estoy de acuerdo con usted al señalar la primera y última línea de mi respuesta.
desde el
1
No creo que la scratchcadena realmente logre nada aquí. El número de reasignaciones de la cadena principal dependerá en gran medida de su tamaño final, no del número de operaciones de adición, a menos que la stringimplementación sea realmente pobre (es decir, no utilice un crecimiento exponencial). Así que "agrupar" appendno ayuda porque una vez que el subyacente stringes grande, solo crecerá ocasionalmente de cualquier manera. Además de eso, agrega un montón de operaciones de copia redundantes y puede haber más reasignaciones (por lo tanto, llamadas a new/ delete) ya que está agregando una cadena corta.
BeeOnRope
@BeeOnRope Estoy de acuerdo contigo.
iain
Estoy bastante seguro de str.reserve(1024);que sería más rápido que esta cosa
hanshenrik
160

La forma en C ++ sería usar std :: stringstream o simplemente concatenaciones de cadenas simples. Las cadenas de C ++ son mutables, por lo que las consideraciones de rendimiento de la concatenación son menos preocupantes.

con respecto al formateo, puede hacer el mismo formateo en una secuencia, pero de una manera diferente, similar acout . o puede usar un functor fuertemente tipado que encapsula esto y proporciona una interfaz tipo String.Format, por ejemplo, boost :: format

jk.
fuente
59
Las cadenas de C ++ son mutables : exactamente. Toda la razón StringBuilderexiste es para cubrir la ineficiencia del tipo de cadena básica inmutable de Java . En otras palabras, StringBuilderes un mosaico, por lo que deberíamos estar contentos de no necesitar tal clase en C ++.
bobobobo
57
Sin embargo, las cadenas inmutables de @bobobobo tienen otros beneficios, sus caballos para cursos
jk.
8
¿Las concatenaciones de cadenas simples no crean un nuevo objeto, entonces el mismo problema que con la inmutabilidad en Java? Considere que todas las variables son cadenas en el siguiente ejemplo: a = b + c + d + e + f; ¿No va a llamar al operador + en byc, luego al operador + en el resultado yd, etc.?
Serge Rogatch
9
Espere un minuto, la clase de cadena estándar sabe cómo mutar, pero eso no significa que la ineficiencia no esté allí. Hasta donde sé, std :: string no puede simplemente extender el tamaño de su char * interno. Eso significa que mutarlo de una manera que requiere más caracteres requiere una reasignación y copia. No es diferente a un vector de caracteres y sin duda es mejor reservar el espacio que necesita en ese caso.
Trygve Skogsholm
77
@ TrygveSkogsholm: no es diferente de un vector de caracteres, pero, por supuesto, la "capacidad" de la cadena puede ser mayor que su tamaño, por lo que no todos los apéndices necesitan una reasignación. En general, las cadenas utilizarán una estrategia de crecimiento exponencial, por lo que el agregado aún se amortiza en una operación de costo lineal. Eso es diferente a las cadenas inmutables de Java en las que cada operación de agregar necesita copiar todos los caracteres en ambas cadenas a una nueva, por lo que una serie de anexos termina como O(n)en general.
BeeOnRope
93

La std::string.appendfunción no es una buena opción porque no acepta muchas formas de datos. Una alternativa más útil es usar std::stringstream; al igual que:

#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
fuente
43

std::string es el equivalente de C ++: es mutable.

dan04
fuente
13

Puede usar .append () para simplemente concatenar cadenas.

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

Creo que incluso podrías hacer:

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

En cuanto a las operaciones de formateo de C # 's StringBuilder, creo que snprintf(o sprintfsi quieres arriesgarte a escribir un código defectuoso ;-)) en una matriz de caracteres y volver a convertirlo en una cadena es la única opción.

Andy Shellam
fuente
Sin embargo, no de la misma manera que printf o .NET's String.Format, ¿verdad?
Andy Shellam
1
es un poco falso decir que son la única forma
jk.
2
@jk: son la única forma de comparar la capacidad de formateo de StringBuilder de .NET, que es lo que la pregunta original hizo específicamente. Dije "Creo" para poder estar equivocado, pero ¿puede mostrarme una forma de obtener la funcionalidad de StringBuilder en C ++ sin usar printf?
Andy Shellam
actualicé mi respuesta para incluir algunas opciones de formato alternativas
jk.
6

Como std::stringen C ++ es mutable, puede usar eso. Tiene un += operatory unappend función.

Si necesita agregar datos numéricos, use el std::to_string funciones.

Si desea aún más flexibilidad en la forma de poder serializar cualquier objeto en una cadena, use la std::stringstreamclase. Pero necesitará implementar sus propias funciones de operador de transmisión para que funcione con sus propias clases personalizadas.

Daemin
fuente
4

std :: string's + = no funciona con const char * (lo que parece ser "string to add"), por lo que definitivamente usar stringstream es lo más cercano a lo que se requiere: simplemente use << en lugar de +

sergeys
fuente
3

Un conveniente generador de cadenas para c ++

Como muchas personas respondieron antes, std :: stringstream es el método de elección. Funciona bien y tiene muchas opciones de conversión y formato. Sin embargo, en mi opinión, tiene un defecto bastante inconveniente: no puedes usarlo como una sola línea o como una expresión. Siempre tienes que escribir:

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

lo cual es bastante molesto, especialmente cuando quieres inicializar cadenas en el constructor.

La razón es que a) std :: stringstream no tiene operador de conversión a std :: string yb) los operadores << () del stringstream no devuelven una referencia de stringstream, sino una referencia std :: ostream - que no se puede calcular más como una secuencia de cadena.

La solución es anular std :: stringstream y darle mejores operadores de coincidencia:

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

Con esto, puedes escribir cosas como

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

incluso en el constructor.

Tengo que confesar que no medí el rendimiento, ya que todavía no lo he usado en un entorno que hace un uso intensivo de la construcción de cadenas, pero supongo que no será mucho peor que std :: stringstream, ya que todo está hecho a través de referencias (excepto la conversión a cadena, pero también es una operación de copia en std :: stringstream)

usuario2328447
fuente
Esto está bien. No veo por qué std::stringstreamno se comporta de esta manera.
Einpoklum
1

El contenedor Rope puede valer si tiene que insertar / eliminar una cadena en el lugar aleatorio de la cadena de destino o para una secuencia de caracteres larga. Aquí hay un ejemplo de la implementación 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
fuente
0

Quería agregar algo nuevo por lo siguiente:

En un primer intento no pude vencer

std::ostringstream 's operator<<

eficiencia, pero con más intentos pude hacer un StringBuilder que es más rápido en algunos casos.

Cada vez que agrego una cadena, solo almaceno una referencia en ella en algún lugar y aumento el contador del tamaño total.

La forma real en que finalmente lo implementé (¡Horror!) Es usar un búfer opaco (std :: vector <char>):

  • Encabezado de 1 byte (2 bits para indicar si los siguientes datos son: cadena, cadena o byte movida [])
  • 6 bits para saber la longitud del byte []

para el byte []

  • Almacene directamente bytes de cadenas cortas (para acceso secuencial a memoria)

para cadenas movidas (cadenas agregadas con std::move)

  • El puntero a un std::stringobjeto (tenemos propiedad)
  • establecer una bandera en la clase si hay bytes reservados no utilizados allí

para cuerdas

  • El puntero a un std::stringobjeto (sin propiedad)

También hay una pequeña optimización, si la última cadena insertada se movió, comprueba los bytes libres reservados pero no utilizados y almacena más bytes allí en lugar de usar el búfer opaco (esto es para ahorrar algo de memoria, en realidad lo hace un poco más lento , tal vez dependa también de la CPU, y de todos modos es raro ver cadenas con espacio reservado adicional)

Finalmente, esto fue un poco más rápido que, std::ostringstreampero tiene algunas desventajas:

  • Asumí tipos de caracteres de longitud fija (por lo tanto, 1,2 o 4 bytes, no son buenos para UTF8), no estoy diciendo que no funcionará para UTF8, solo que no lo comprobé por pereza.
  • Utilicé una mala práctica de codificación (buffer opaco, fácil de hacer que no sea portátil, creo que el mío es portátil por cierto)
  • Carece de todas las características de ostringstream
  • Si alguna cadena referenciada se elimina antes de fusionar todas las cadenas: comportamiento indefinido.

¿conclusión? utilizar std::ostringstream

Ya solucionó el mayor cuello de botella, mientras que obtener pocos puntos porcentuales de velocidad con la implementación de la mina no vale la pena.

CoffeDeveloper
fuente