32

¿Cuál es la forma más eficiente de separar un string en c++? como el que se muestra en el código, el cual contiene espacios entre las palabras, este es el método que yo estoy usando actualmente.

#include <iostream>
#include <sstream>

int main(int argc, char** argv){

    std::string str("Texto para dividir");
    std::istringstream isstream(str);

    while(!isstream.eof()){

        std::string tempStr;

        isstream >> tempStr;

        std::cout << tempStr << std::endl;
    }

    return 0;
}
Chofoteddy
  • 5,975
  • 5
  • 25
  • 65
Angel Angel
  • 9,623
  • 9
  • 37
  • 68
  • 3
    Cuidado con usar espacios para separar palabras, hay varios idiomas (como chino) que no usan espacios. Incluso en idiomas con caracteres latinos hay palabras como parlez-vous francais? en el que no se usa el espacio como separador. ICU tiene una libreria genial para todos los casos. http://userguide.icu-project.org/boundaryanalysis – Miguel Garcia Dec 10 '15 at 16:34

7 Answers7

27

Lo de elegancia es algo difícil de decir, es cuestión de gustos. Pero la legibilidad del código siempre es importante. Por eso, este código se puede simplificar así: (a mi me parece más legible)

std::string tempStr;
while(isstream >> tempStr) {
    std::cout << tempStr << std::endl;
}

Con lo cual, metiendo todo en una función quedaría algo así:

std::vector<std::string> split_istringstream(std::string str) {
    std::vector<std::string> resultado;
    std::istringstream isstream(str);
    std::string palabra;

    while(isstream >> palabra){
        resultado.push_back(palabra);
    }

    return resultado;
}

Otra alternativa, que incluso permite definir el delimitador (el operador >> de istringstream solo divide la cadena por los espacios - ASCII 32) es utilizar std::getline.

std::vector<std::string> split_getline(std::string str, char delim) {
    std::vector<std::string> resultado;
    std::istringstream isstream(str);
    std::string palabra;

    while(std::getline(isstream, palabra, delim)){
        resultado.push_back(palabra);
    }

    return resultado;
}

Aquí abajo te dejo una comparativa entre ambas opciones, son prácticamente iguales en eficiencia, dejando de un lado los vectores, reservan prácticamente la misma memoria.

http://cpp.sh/3jtw

A. Cedano
  • 86,578
  • 19
  • 122
  • 221
rnrneverdies
  • 16,491
  • 3
  • 49
  • 79
23

Normalmente lo eficiente y lo bonito no suelen ir dados de la mano. El código bonito suele tener código excesivo (al menos 'por detrás'), pero en ese caso lo que prima es la legibilidad del código... cuando se persigue la eficiencia lo que se acaba consiguiendo es código menos legible... pero más eficiente (en esto habría que especificiar si la eficiencia se refiere a velocidad de ejecución o a consumo de memoria).

Bueno, ya te han puesto códigos bonitos yo te pongo uno que tira por la vía de la rapidez. ¿Cómo de rápido? pues más o menos un 30%-40% más rápido que la opción del istringstream........ claro que el código ya no queda tan bonito:

std::vector<std::string>
split_iterator(const std::string& str)
{
  std::vector<std::string> resultado;

  std::string::const_iterator itBegin = str.begin();
  std::string::const_iterator itEnd   = str.end();

  int numItems = 1;
  for( std::string::const_iterator it = itBegin; it!=itEnd; ++it )
    numItems += *it==' ';

  resultado.reserve(numItems);

  for( std::string::const_iterator it = itBegin; it!=itEnd; ++it )
  {
    if( *it == ' ' )
    {
      resultado.push_back(std::string(itBegin,it));
      itBegin = it+1;
    }
  }

  if( itBegin != itEnd )
    resultado.push_back(std::string(itBegin,itEnd));

  return resultado;
}

En este caso uso iteradores, lo que evita el sobrecoste de crear y llamar a los métodos istringstream. Además como optimización adicional, antes de separar el string precalculo el número total de elementos. De esta forma evito que el vector tenga que llamar a realloc continuamente.

Un saludo

Angel Angel
  • 9,623
  • 9
  • 37
  • 68
eferion
  • 49,291
  • 5
  • 30
  • 72
  • 1
    su punto de vista me parece interesante "pues más o menos un 30%-40% más rápido" no lo eh podido testear pero me parece interesante, ademas de lo que dice -> "en esto habría que especificiar si la eficiencia se refiere a velocidad de ejecución o a consumo de memoria" estoy de acuerdo pues lo tendre en cuenta por futuras preguntas, en especificarlo pero gracias por contestar intentare probar su forma, por indagar – Angel Angel Dec 02 '15 at 10:06
  • usted a testeado lo que dice de un 30%? – Angel Angel Dec 02 '15 at 10:23
  • 2
    He creado un `string` con aproximadamente unos 150 elementos, después he creado un programa que llama 1.000.000 de veces a la versión `istringstream` y otras tantas veces a la función que te he puesto. El tiempo de ejecución, en mi máquina de la versión `istringstream` es de unos 43 segundos, mientras que mi función se ejecuta en unos 26 segundos... un 38% menos de tiempo invertido. – eferion Dec 02 '15 at 10:28
  • yo ahora lo estaba testeando por encima y si parece que es rapido gracias por sus datos saludos – Angel Angel Dec 02 '15 at 10:31
17

Usando la clase iterator

#include <iterator>
#include <iostream>
#include <string>
#include <sstream>

int main() {
    using namespace std;
    string s = "Texto para dividir";
    istringstream iss(s);
    copy(istream_iterator<string>(iss),
         istream_iterator<string>(),
         ostream_iterator<string>(cout, "\n"));
}

se tendrá por ejemplo la salida:

Texto
para
dividir

Ver ejemplo online!

Jorgesys
  • 103,630
  • 13
  • 52
  • 124
  • 2
    Su uso de std::copy es muy inteligente, o por lo menos a mi me lo parece, nunca se me habia ocurrido esa manera, gracias por contestar – Angel Angel Dec 02 '15 at 10:10
12

Parecido a la respuesta de Elenasys, también usando iteradores de stream:

template <typename char_type>
using string_collection = std::vector<std::basic_string<char_type>>;

template <typename char_type>
string_collection<char_type> split(const std::basic_string<char_type> &text)
{
    using string = std::basic_string<char_type>;
    using iterator = std::istream_iterator<string, char_type>;

    std::basic_stringstream<char_type> reader(text);
    return {iterator(reader), iterator()};
}

El truco es que se construye en línea el vector<string> en el return de la función split pasando los iteradores de inicio y final del stringstream, esta función split puede usarse así:

int main()
{
    for (const auto &palabra : split("hola don pepito"))
        std::cout << palabra << '\n';
    return 0;
}

Y al usar plantillas debería funcionar con cualquier tipo de cadena (char, wchar_t, char16_t y char32_t).

Puedes ver el código funcionando aquí.

PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
11

Esta técnica utiliza algunos de los métodos anteriores (iterators, std::copy), añadiendo la posibilidad de particularizar cual es el separador y extendiendo el uso de la técnca a streams multilínea que contienen otras cosas que no son std::string.

Por supuesto el std::copy se puede hacer a un std::vector por simplicidad y afinidad con la pregunta original, se dirige todo a std::cout.

Primero la respuesta directa a la pregunta:

#include <iostream>
#include <iterator>
#include <limits>
#include <sstream>
#include <string>


struct SeparatorReader: std::ctype<char>
{
    template<typename T>
    SeparatorReader(const T &seps): std::ctype<char>(get_table(seps), true) {}

    template<typename T>
    std::ctype_base::mask const *get_table(const T &seps) {
        auto &&rc = new std::ctype_base::mask[std::ctype<char>::table_size]();
        for(auto &&sep: seps)
            rc[static_cast<unsigned char>(sep)] = std::ctype_base::space;
        return &rc[0];
    }
};

int
main(int argc, char *argv[])
{
    std::string str("Texto para dividir");
    std::istringstream stream(str);
    // This says whitespace is only ' '
    stream.imbue(std::locale(stream.getloc(), new SeparatorReader(" ")));

    auto first = std::istream_iterator<std::string>(stream);
    auto last = std::istream_iterator<std::string>();
    auto out = std::ostream_iterator<std::string>(std::cout, "\n");

    std::copy(first, last, out);

    return 0;
}

Con el siguiente resultado:

Texto
para
dividir

Nada nuevo bajo el sol, salvo por la introducción del SeparatorReader. Esta subclase de std::ctype acepta una lista de caracteres (que sea iterable, puede incluso ser un vector de caracteres) y los marca como std::ctype_base::space, es decir como los caracteres a considerar como espacio en blanco.

En el código anterior hemos aplicado " " porque es el separador de la cadena de texto de la pregunta (Nota: '\n' no está incluído como espacio en blanco en el ejemplo)

Una vez introducido el concepto podemos trabajar con otro separador como por ejemplo :.

int
main(int argc, char *argv[])
{
    std::string str("Texto:para:dividir");
    std::istringstream stream(str);
    // This says whitespace is only ':'
    stream.imbue(std::locale(stream.getloc(), new SeparatorReader(":")));

    auto first = std::istream_iterator<std::string>(stream);
    auto last = std::istream_iterator<std::string>();
    auto out = std::ostream_iterator<std::string>(std::cout, "\n");

    std::copy(first, last, out);

    return 0;
}

Con el mismo resultado de antes a pesar de haber cambiado los " " por ":".

Texto
para
dividir

La técnica se vuelve más interesante si la utilizamos para aplicársela la tokenización y conversión de otros tipos como p.ej. int y nos apoyamos en los errores de conversión para detectar hitos mayores.

En el siguiente ejemplo simulamos la lectura de un archivo csv que contiene enteros. En lugar de un std::isstringstream podría tratarse de un std:istream vulgar y corriente.

Como SeparatorReader no ve \n como espacio en blanco, lo que producirá un error de conversión que permite detectar el final de línea, que tendremos que ignorar (pasándolo p.ej a un char desechable)

int
main(int argc, char *argv[])
{
    std::string str("1,2, 3\n4, 5,6\n 7,8,9");
    std::istringstream stream(str);
    // Only ' ' and ',' are whitespace, '\n' will stop int conversion
    stream.imbue(std::locale(stream.getloc(), new SeparatorReader(" ,")));

    auto last = std::istream_iterator<int>();
    auto out = std::ostream_iterator<int>(std::cout, "-");

    while(stream) {
        auto first = std::istream_iterator<int>(stream);  // redo after error needed
        std::copy(first, last, out);
        std::cout << std::endl;  // separate lines

        // Either eof or eol - try to skip eol or else re-meet eof
        stream.clear(); char skip; stream >> skip;
    }
    return 0;
}

Con el siguiente resultado:

1-2-3-
4-5-6-
7-8-9-
mementum
  • 555
  • 3
  • 5
11

Puedes usar boost:

std::string line("prueba,prueba2,prueba3");
std::vector<std::string> strs;
boost::split(strs, line, boost::is_any_of(","));

Las cadenas separadas se copian a strs. Con is_any_of puedes especificar el delimitador. Boost es en general muy eficiente y el código te queda bastante limpio y comprensible.

ArthurChamz
  • 500
  • 2
  • 16
1

Una forma de separar un cadena de caracteres es de la siguiente manera

#include <regex>    // para los objetos de expresiones regulares y std::string
#include <vector>   // para el std::vector<std::string>
#include <iterator> // para std::back_inserter
std::vector<std::string> split ( std::string const&, std::regex = std::regex("[^\\s]")
{
   std::vector<std::string> splitted:
   for ( auto it = std::sregex_iterator(str.begin(), str.end(), re);
         it != std::sregex_iterator(); ++it )
   {
      std::back_inserter( splitted ) = it->str();
   }
   return splitted;
}
Emanuel Gauler
  • 151
  • 1
  • 9