Cómo extraer palabras entre espacios en una cadena de C++

Cómo extraer palabras entre espacios en una cadena de C++

Ya vimos cómo dividir una cadena en palabras con un delimitador, pero hay otro caso de uso que está bastante cerca y que no tiene la misma implementación:extraer palabras que están entre espacios en una cadena.

Por ejemplo, de la siguiente cadena:

"word1    word2   word3  "

Nos gustaría extraer 3 subcadenas:"palabra1", "palabra2" y "palabra3".

Lo haremos de dos maneras:la primera es generar una colección de std::strings , y el otro para generar una colección de std::string_view s.

Este es un ejercicio interesante porque permite pensar en cómo escribir código expresivo, en particular con buenos nombres y usar algoritmos STL. Y antes de ver una solución, ¡tendrá la oportunidad de codificarla usted mismo!

Extracción de palabras como cadenas

Diseñemos el extractWords función, que toma una cadena y pesca entre espacios las palabras que contiene.

La interfaz

¿Cómo debería ser la interfaz de la función? Su entrada es la cadena a recorrer y su salida es una colección de palabras.

En general, debemos esforzarnos por que las funciones generen sus resultados a través de sus tipos de salida. Pero en este caso, ¿cuál es el tipo de retorno? ¿Debería ser un std::vector<std::string>? ? Eso suena como una opción razonable. Pero, ¿y si queremos poner los resultados en un std::set ? La idea de crear un intermediario std::vector no es tan seductor.

¿O qué pasa si queremos enviar la salida a un flujo? Una vez más, un vector intermediario potencialmente grande no es una idea atractiva.

Para resolver este problema, construiremos nuestra función sobre el modelo del algoritmo STL:usando un iterador de salida. Este iterador es un parámetro de plantilla y podría ser cualquier cosa:el begin de un vector, un back_inserter , un stream_iterator , un iterador de salida inteligente...

Así es como se verá la interfaz:

template <typename OutputIterator>
void extractWords(std::string const& s, OutputIterator out)

Tenga en cuenta que algunos algoritmos STL devuelven un OutputIterator , para producir una posición interesante en la colección de salida con respecto al algoritmo. Por ejemplo, std::partition devuelve el punto de partición y std::rotate devuelve la nueva posición del elemento que solía estar al principio de la colección.

Pero en nuestro caso, no estoy seguro de que haya una posición particularmente interesante en esta colección. Si ve uno, hágamelo saber y veremos si podemos devolverlo desde el algoritmo. Pero por el momento, limitémonos a devolver void .

Pruébelo con pruebas

¿Podría pensar en una forma de implementar extractWords ? Me tomó varias iteraciones antes de llegar a una solución aquí, y lo que más me ayudó fue tener un conjunto de pruebas unitarias para probar diferentes soluciones y refinar la función, con retroalimentación instantánea sobre si es correcta.

Es genial tener un marco de pruebas unitarias en sus proyectos, como Catch2 o Gtest, por ejemplo, pero si desea probar algún código en un entorno limitado en línea, no se detenga si no puede usar un marco de pruebas. Siempre puede piratear una función que pruebe su código y devuelva un booleano para indicar si las pruebas pasaron o no. El punto es tener algunos comentarios sobre sus modificaciones, y rápidamente.

¡Prueba a implementar la función! Puedes usar este patio de recreo que contiene algunas pruebas básicas:


(Como comentarios para futuros artículos, ¿agradecería tener la oportunidad de escribir el código en un espacio aislado incrustado en la página? ¿Cómo podemos mejorar su experiencia de usuario con respecto a esto?)

Recorriendo la colección

Aquí hay una posible solución.

Para decidir si un carácter es una letra o un espacio, usemos esta lambda siguiente:

static auto const isSpace = [](char letter){ return letter == ' '; };

Tenga en cuenta que podríamos haberlo definido como una función simple, pero la lambda permite que se defina dentro de extractWords . Encuentro que esto muestra que se relaciona con nuestro algoritmo, reduce el lapso entre la definición y el uso, y no contamina el espacio de nombres externo.

También tenga en cuenta que is_space solo trata con un tipo de espaciado (no tabulaciones, retornos de línea, etc.), pero no es difícil tratar con más tipos y parametrizar nuestra función con esta lambda.

Entonces, comencemos por ubicar la primera palabra. El subrango donde se encuentra la primera palabra comienza en el primer carácter que no está en blanco y termina en el primer carácter en blanco:

auto const beginWord = std::find_if_not(begin(s), end(s), isSpace);
auto const endWord = std::find_if(beginWord, end(s), isSpace);

beginWord y endWord son iteradores. Tenga en cuenta que no los llamamos it o it1 o it2 , pero les damos nombres significativos para mostrar lo que representan dentro de la colección.

Si beginWord y endWord son diferentes, entonces tenemos una palabra aquí. Necesitamos enviarlo al iterador de salida, que espera un std::string :

*out = std::string(beginWord, endWord);

Y tenemos que incrementar ese iterador de salida, para avanzar en la colección de salida:

++out;

Hasta ahora, el código ensamblado se ve así:

static auto const isSpace = [](char letter){ return letter == ' '; };

auto const beginWord = std::find_if_not(begin(s), end(s), isSpace);
auto const endWord = std::find_if(beginWord, end(s), isSpace);
if (beginWord != endWord)
{
    *out = std::string(beginWord, endWord);
    ++out;
}

Este código permite encontrar la primera palabra en la cadena. Ahora necesitamos hacer que repita todas las palabras que contiene la cadena.

El bucle

Después de algunas iteraciones para enderezar el ciclo, aquí hay una posible solución para implementar extractWords :

template <typename OutputIterator>
void extractWords(std::string const& s, OutputIterator out)
{
    static auto const isSpace = [](char letter){ return letter == ' '; };
    
    auto lastExaminedPosition = begin(s);
    while (lastExaminedPosition != end(s))
    {
        auto const beginWord = std::find_if_not(lastExaminedPosition, end(s), isSpace);
        auto const endWord = std::find_if(beginWord, end(s), isSpace);
        if (beginWord != endWord)
        {
            *out = std::string(beginWord, endWord);
            ++out;
        }
        lastExaminedPosition = endWord;
    }
}

Nuevamente, no es que no tengamos que llamar a nuestros iteradores it . Un nombre como lastExaminedPosition es más explícito.

Otra posibilidad es deshacerse del if y combínalo con la condición del ciclo:

template <typename OutputIterator>
void extractWords(std::string const& s, OutputIterator out)
{
    static auto const isSpace = [](char letter){ return letter == ' '; };
    
    auto beginWord = std::find_if_not(begin(s), end(s), isSpace);
    while (beginWord != end(s))
    {
        auto const endWord = std::find_if(beginWord, end(s), isSpace);
        *out = std::string(beginWord, endWord);
        ++out;
        beginWord = std::find_if_not(endWord, end(s), isSpace);
    }    
}

Pero me gusta más la primera solución, porque la segunda duplica algo de código (la llamada a find_if_not ), y su flujo es posiblemente más difícil de seguir. ¿Qué opinas?

Extracción de palabras como std::string_view s

Si la cadena la pasamos a extractWords no es un objeto temporal, podríamos querer obtener una colección de C++17 std::string_view s, para evitar crear nuevos std::string s.

El algoritmo en sí no cambia. La parte que cambia es cómo enviamos el resultado al iterador de salida:

template <typename OutputIterator>
void extractWordViews(std::string const& s, OutputIterator out)
{
    static auto const isSpace = [](char letter){ return letter == ' '; };
    
    auto lastExaminedPosition = begin(s);
    while (lastExaminedPosition != end(s))
    {
        auto const beginWord = std::find_if_not(lastExaminedPosition, end(s), isSpace);
        auto const endWord = std::find_if(beginWord, end(s), isSpace);
        if (beginWord != endWord)
        {
            *out = std::string_view(&*beginWord, std::distance(beginWord, endWord));
            ++out;
        }
        lastExaminedPosition = endWord;
    }
}

Tenga en cuenta que tener extractWords y extractWordViews ofrece flexibilidad, pero también conlleva un riesgo:si usa extractWords con un vector de std::string_view el código compilará:

std::vector<std::string_view> results;
extractWords(s, back_inserter(results));

Pero conduce a un comportamiento indefinido, porque el std::string_view La salida en el vector se referirá al std::string temporal s de salida por el algoritmo en esa línea:

*out = std::string(beginWord, endWord);

y ese temporal std::string se fue hace mucho tiempo cuando extractWords finaliza su ejecución (fue destruido al final de la sentencia donde fue creado). Si ve cómo podemos evitar una llamada a extractWords de la compilación cuando lo conectamos a un contenedor de string_view por accidente, deja un comentario en la sección de comentarios a continuación.

Grupos de información

extractWords es un algoritmo que atraviesa una colección, buscando bloques de elementos especiales agrupados. Pero está lejos de ser el único. Otro ejemplo es adjacent_merge , que examinaremos en una publicación futura.

Si tiene otros ejemplos de tales algoritmos, ¡hágamelo saber! Al analizar varios de ellos, podemos ver algunos patrones y encontrar buenas generalizaciones y nuevas abstracciones, para hacer que su código sea más expresivo.

También te puede gustar

  • Cómo dividir una cadena en C++
  • Cómo (std::)encontrar algo eficientemente con STL
  • El recurso de aprendizaje STL
  • Haz que tus funciones sean funcionales