std::format y el nuevo estándar de formateo en C++

Olvídate de la inseguridad de printf y de la sintaxis pesada de std::stringstream. En C++20 [C++20] se introdujo la librería <format>, que nos da un sistema de formateo de strings type-safe, extensible y con una sintaxis inspirada en Python que es mucho más legible.

std::format es una función que devuelve un std::string formateado utilizando un marcador de posición {}. A diferencia de printf, donde un error en el especificador (como pasar un int donde esperas un %s) resulta en un desastre de memoria o comportamiento indefinido, std::format comprueba los tipos en tiempo de compilación (si la cadena es constante) o lanza una excepción std::format_error si algo falla en tiempo de ejecución. Es el equilibrio perfecto entre la potencia de printf y la seguridad de la STL.

Para usarlo, simplemente pasas una cadena de formato y los argumentos. Si necesitas algo más complejo, puedes usar especificadores dentro de los corchetes: {:>10} para alinear a la derecha con un ancho de 10, {:05} para rellenar con ceros, o {:x} para hexadecimal. Si quieres máxima eficiencia y no quieres alocar una cadena nueva, puedes usar std::format_to para escribir directamente en un buffer existente. Si el formato solo se conoce en tiempo de ejecución (por ejemplo, viene de un archivo de configuración), recurrimos a std::vformat con std::make_format_args.

¿Cuándo deberías usarlo? Siempre que necesites construir strings para logs, mensajes de usuario o serialización de datos. ¿Qué pasa si lo usas mal? Si intentas formatear un tipo que no conoce, el compilador te dará un error muy largo pero muy descriptivo sobre la falta de una especialización de std::formatter, evitando que el programa falle silenciosamente en producción.

#include <format>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

// Un tipo personalizado para demostrar la extensibilidad de std::format
struct Sensor {
    std::string nombre;
    double valor;
    int id;
};

// Para que std::format reconozca nuestro tipo, debemos especializar std::formatter
template <>
struct std::formatter<Sensor> {
    // El método parse es obligatorio; aquí simplemente aceptamos el formato por defecto
    constexpr auto parse(format_parse_context& ctx) {
        return ctx.begin();
    }

    // El método format define cómo se escribe el objeto en el iterador de salida
    auto format(const Sensor& s, format_context& ctx) const {
        // Usamos format_to para escribir directamente en el buffer del contexto
        return std::format_to(ctx.out(), "[ID: {} | {} = {:.2f}]", s.id, s.nombre, s.valor);
    }
};

int main() {
    // 1. Formateo básico y alineación
    std::string name = "Sistema";
    int version = 5;
    
    // Uso de argumentos posicionales {0} y {1}
    // Alineación: {:^10} centrado, {:<10} izquierda, {:>10} derecha
    std::string info = std::format("Nombre: {:<10} | Versión: {1:03d}", name, version);
    std::cout << info << "\n";

    // 2. Formateo numérico avanzado
    double pi = 3.1415926535;
    int hex_val = 255;
    int bin_val = 7;

    std::cout << std::format("Pi: {:.4f}, Hex: {:x}, Bin: {:b}\n", pi, hex_val, bin_val);

    // 3. Uso de tipos personalizados
    Sensor temp{"Temperatura", 25.567, 101};
    std::cout << std::format("Estado actual: {}\n", temp);

    // 4. std::format_to para escribir en un buffer preexistente (evita alocaciones extra)
    std::string buffer = "Log: ";
    std::format_to(std::back_inserter(buffer), "Evento detectado en sensor {}", temp.id);
    std::cout << buffer << "\n";

    // Nota sobre C++23:
    // Si tu compilador lo soporta, en lugar de std::cout << std::format(...),
    // puedes usar std::print("Hola, {}!\n", "mundo");
    
    return 0;
}

Desglose del código

En el ejemplo, lo primero que notarás es la especialización de std::formatter<Sensor>. Este es el “secreto” de la extensibilidad de la librería. El método parse es crucial: es lo que el compilador llama para analizar los especificadores de formato (como :.2f) dentro de las llaves. Si no vas a implementar un parser complejo, simplemente retornamos ctx.begin(). El método format recibe un format_context y utiliza std::format_to para volcar los datos en el iterador de salida (ctx.out()). Esto es extremadamente eficiente porque escribe directamente en el buffer de destino sin crear objetos intermedios.

En main, observa cómo std::format gestiona la alineación. El especificador {: <10} le dice al motor de formateo que el campo debe ocupar 10 caracteres, alineado a la izquierda. Al usar {:03d}, estamos forzando que el entero tenga un ancho mínimo de 3, rellenando con ceros a la izquierda si es necesario.

Fíjate también en los argumentos posicionales: en la línea de info, usamos {1:03d}. Esto le indica que debe tomar el segundo argumento pasado (version) y aplicarle el formato de un entero con 3 ceros de relleno. Esto es vital cuando el orden de los argumentos en la cadena de formato depende del idioma del usuario (por ejemplo, en una traducción de inglés a español, el orden de los elementos en una frase puede cambiar).

Finalmente, el uso de std::format_to con std::back_inserter(buffer) demuestra cómo podemos extender un std::string existente. Esto es mucho más eficiente que hacer str = std::format(str + ...) porque evitamos la creación de múltiples strings temporales y las múltiples realocaciones de memoria en el heap.

El error frecuente

Un error muy común cuando se empieza con std::format es intentar usarlo con un tipo propio sin haber especializado std::formatter.

struct Data { int val; };
Data d{10};
// Error de compilación masivo: "invalid type argument of unary '-' to non-pointer parameter..."
// O algo similar indicando que no se puede formatear el tipo 'Data'
std::string s = std::format("{}", d); 

A diferencia de printf, que simplemente causaría un crash o basura en pantalla si le pasas un objeto de una struct, std::format fallará en el proceso de compilación. El error es más difícil de leer inicialmente debido a la complejidad de los templates de la STL, pero es mucho más seguro. Si el error es en tiempo de ejecución (por ejemplo, al usar vformat con una cadena de formato dinámica mal construida), lanzará una std::format_error.

Para capturar estos errores en tiempo de ejecución, asegúrate de envolver tus llamadas en un bloque try-catch.

79

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio