Cadenas en C++: trim, split, join, mayúsculas, std::format, conversiones y regex

Las tareas de todos los días con cadenas en C++, resueltas con C++20 y C++23: quitar espacios, partir y unir, buscar y reemplazar, pasar a mayúsculas (y por qué falla con las tildes), formatear con std::format, convertir texto en números sin sorpresas y expresiones regulares básicas.

En C++ las cadenas son std::string, que guarda y libera su memoria sola, y std::string_view, una vista que apunta a una cadena ajena sin copiarla. Las recetas de esta página usan las dos: string_view para leer sin copiar y string para devolver resultados.

Quitar espacios: trim

#include <print>
#include <string>
#include <string_view>

std::string_view trim(std::string_view s)
{
    const char* espacios = " \t\r\n\f\v";
    auto ini = s.find_first_not_of(espacios);
    if (ini == std::string_view::npos) return {};
    auto fin = s.find_last_not_of(espacios);
    return s.substr(ini, fin - ini + 1);
}

int main()
{
    for (std::string_view s : {"  hola mundo \n", "\t\t", "sin espacios"}) {
        std::println("[{}]", trim(s));
    }
}
salida[hola mundo]
[]
[sin espacios]

Devolver un string_view evita copiar, pero tiene una condición: la cadena original tiene que seguir viva mientras uses la vista. Si haces auto v = trim(leer_linea()); y leer_linea() devuelve un std::string temporal, v apunta a memoria ya liberada. En ese caso, guárdalo en un std::string: std::string limpio{trim(...)};.

Partir una cadena sin perder campos vacíos

#include <print>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>

std::vector<std::string> partir(std::string_view s, char sep)
{
    std::vector<std::string> partes;
    for (auto trozo : s | std::views::split(sep)) {
        partes.emplace_back(trozo.begin(), trozo.end());
    }
    return partes;
}

int main()
{
    auto p = partir("ana;;luis;", ';');
    std::println("{} partes: {}", p.size(), p);
    std::println("{}", partir("sin separador", ';'));
}
salida4 partes: ["ana", "", "luis", ""]
["sin separador"]

std::views::split (C++20) conserva los campos vacíos: entre dos ; seguidos hay un campo vacío, y después del último ; otro. Es lo correcto para datos con columnas, donde un campo vacío significa «sin valor» y saltárselo descolocaría todos los demás. Si quieres partir por palabras, ignorando espacios repetidos, es más cómodo std::istringstream con >>.

Unir: join

#include <print>
#include <ranges>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> frutas{"manzana", "pera", "uva"};

    auto unida = frutas | std::views::join_with(std::string(", ")) | std::ranges::to<std::string>();
    std::println("{}", unida);

    // a mano, para compiladores sin join_with (antes de GCC 13 o Clang 18)
    std::string r;
    for (const auto& f : frutas) {
        if (!r.empty()) r += " | ";
        r += f;
    }
    std::println("{}", r);
}
salidamanzana, pera, uva
manzana | pera | uva

Buscar, empezar por y reemplazar

#include <print>
#include <string>

std::string reemplazar_todo(std::string s, std::string_view de, std::string_view a)
{
    if (de.empty()) return s;
    for (auto pos = s.find(de); pos != std::string::npos; pos = s.find(de, pos + a.size())) {
        s.replace(pos, de.size(), a);
    }
    return s;
}

int main()
{
    std::string url = "https://decodigo.com/c-plus-plus";
    std::println("empieza por https: {}", url.starts_with("https://"));   // C++20
    std::println("contiene «plus»: {}", url.contains("plus"));             // C++23
    std::println("posición de «.com»: {}", url.find(".com"));
    std::println("{}", reemplazar_todo("a-b-c-d", "-", " - "));
}
salidaempieza por https: true
contiene «plus»: true
posición de «.com»: 16
a - b - c - d

find devuelve std::string::npos cuando no encuentra nada, que es el mayor número que cabe en un size_t. Compara siempre con npos, nunca con -1 guardado en un int. En reemplazar_todo, la búsqueda continúa después del texto ya reemplazado; si empezara desde pos, reemplazar «a» por «aa» entraría en un bucle infinito.

Mayúsculas y minúsculas, y el problema de los acentos

#include <algorithm>
#include <cctype>
#include <print>
#include <string>

std::string mayusculas(std::string s)
{
    std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::toupper(c); });
    return s;
}

int main()
{
    std::println("{}", mayusculas("hola mundo"));
    std::println("{}", mayusculas("año, canción, pingüino"));
    std::println("«ñ» ocupa {} bytes", std::string("ñ").size());
}
salidaHOLA MUNDO
AñO, CANCIóN, PINGüINO
«ñ» ocupa 2 bytes

std::toupper trabaja byte a byte, y en UTF-8 la «ñ», las vocales con tilde y la «ü» ocupan dos bytes cada una: se quedan como estaban. Para cambiar mayúsculas de verdad con cualquier idioma hace falta una biblioteca que entienda Unicode, como ICU. Fíjate también en el unsigned char del parámetro de la lambda: pasar a toupper un char negativo (los bytes de la «ñ» lo son) es comportamiento indefinido.

Formatear con std::format

#include <format>
#include <print>
#include <string>

int main()
{
    std::string s = std::format("{} tiene {} años", "Ana", 34);
    std::println("{}", s);
    std::println("[{:>8}] [{:<8}] [{:^8}]", "der", "izq", "centro");
    std::println("[{:08.3f}] [{:+d}] [{:#x}] [{:b}]", 3.14159, 42, 255, 10);
    std::println("{1} antes que {0}", "segundo", "primero");
}
salidaAna tiene 34 años
[     der] [izq     ] [ centro ]
[0003.142] [+42] [0xff] [1010]
primero antes que segundo

std::format (C++20) y std::println (C++23) usan la misma sintaxis, heredada de Python: {} para cada valor, y después de los dos puntos la alineación, el ancho, la precisión y la base. A diferencia de printf, el compilador comprueba la cadena de formato: si pones más {} que valores, el programa no compila.

Convertir texto en números: stoi frente a from_chars

#include <charconv>
#include <print>
#include <string>

int main()
{
    // std::stoi convierte lo que puede y se calla el resto
    std::println("stoi(\"12abc\") = {}", std::stoi("12abc"));
    try {
        std::stoi("abc");
    } catch (const std::invalid_argument&) {
        std::println("stoi(\"abc\") lanza invalid_argument");
    }
    try {
        std::stoi("99999999999");
    } catch (const std::out_of_range&) {
        std::println("stoi(\"99999999999\") lanza out_of_range");
    }

    // std::from_chars no lanza y dice dónde se detuvo
    for (std::string s : {"12abc", "abc", "99999999999", "-42"}) {
        int n = 0;
        auto [fin, error] = std::from_chars(s.data(), s.data() + s.size(), n);
        if (error == std::errc::invalid_argument) {
            std::println("from_chars(\"{}\"): no es un número", s);
        } else if (error == std::errc::result_out_of_range) {
            std::println("from_chars(\"{}\"): demasiado grande", s);
        } else if (fin != s.data() + s.size()) {
            std::println("from_chars(\"{}\"): {} y sobra «{}»", s, n, fin);
        } else {
            std::println("from_chars(\"{}\"): {}", s, n);
        }
    }
}
salidastoi("12abc") = 12
stoi("abc") lanza invalid_argument
stoi("99999999999") lanza out_of_range
from_chars("12abc"): 12 y sobra «abc»
from_chars("abc"): no es un número
from_chars("99999999999"): demasiado grande
from_chars("-42"): -42

std::stoi("12abc") devuelve 12 sin quejarse: para validar lo que escribe un usuario no sirve. std::from_chars (C++17) no lanza excepciones, no depende del idioma del sistema y devuelve un puntero al primer carácter que no pudo convertir, así que comprobar que se usó toda la cadena es una comparación. Es también la forma más rápida de convertir números en C++.

Expresiones regulares básicas

#include <print>
#include <regex>
#include <string>

int main()
{
    std::regex fecha(R"((\d{4})-(\d{2})-(\d{2}))");
    std::string texto = "Entregas: 2026-10-30 y 2026-11-15.";

    for (std::sregex_iterator it(texto.begin(), texto.end(), fecha), fin; it != fin; ++it) {
        const std::smatch& m = *it;
        std::println("día {} del mes {} de {}", m[3].str(), m[2].str(), m[1].str());
    }

    std::regex codigo(R"([A-Z]{2}-\d{3})");
    for (std::string c : {"AB-123", "ab-123", "AB-1234"}) {
        std::println("{}: {}", c, std::regex_match(c, codigo) ? "válido" : "no válido");
    }
}
salidadía 30 del mes 10 de 2026
día 15 del mes 11 de 2026
AB-123: válido
ab-123: no válido
AB-1234: no válido

regex_match exige que la expresión cubra la cadena entera, y regex_search o sregex_iterator buscan coincidencias dentro. Las cadenas en bruto, R"(...)", evitan tener que doblar cada barra invertida. Dos advertencias: std::regex es lento comparado con otras bibliotecas (si lo usas mucho, compila la expresión una vez y reutilízala, como aquí) y trabaja con bytes, así que \w no reconoce la «ñ» ni las vocales con tilde de un texto en UTF-8.

Resumen

  • TareaEn C++ modernoCuidado con
  • Quitar espaciosfind_first_not_of y find_last_not_ofEl string_view devuelto no debe sobrevivir a la cadena
  • Partirstd::views::splitConserva los campos vacíos (casi siempre es lo que quieres)
  • Unirviews::join_with + ranges::toNecesita GCC 13 o Clang 18
  • Mayúsculasstd::toupper con unsigned charNo sirve para letras con tilde en UTF-8
  • Formatearstd::format, std::printlnLlaves literales: {{ y }}
  • Texto a númerostd::from_charsstoi acepta «12abc» sin avisar

Todo el código de esta página se compiló con GCC 15.3 (C++23, -std=c++23 -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.