printf no es type-safe: si el tipo del argumento no coincide con el especificador de formato, el comportamiento es indefinido. Además, no funciona con tipos definidos por el usuario sin extensiones no estándar. cout con operadores << es seguro pero verboso, lento en casos críticos y difícil de leer cuando el formato es complejo.
std::format, añadido en C++20, resuelve los dos problemas: es type-safe, extensible a tipos propios y tiene una sintaxis compacta inspirada en Python y en la librería {fmt} de Victor Zverovich (que pasó a ser la base del estándar).
Uso básico
#include <format>
#include <string>
#include <print> // C++23
int main() {
// std::format devuelve un std::string
std::string s = std::format("Hola, {}!", "mundo");
// std::print y std::println (C++23) escriben directamente a stdout
std::println("{} + {} = {}", 2, 3, 5);
std::print(stderr, "Error en línea {}n", 42);
// También puedes escribir a un buffer
std::string buf;
std::format_to(std::back_inserter(buf), "valor: {}", 99);
}
Especificadores de formato
La sintaxis general es {[índice]:[relleno][alineación][ancho][.precisión][tipo]}. Ejemplos prácticos:
// Enteros
std::format("{}", 42); // "42"
std::format("{:d}", 42); // "42" (decimal explícito)
std::format("{:x}", 255); // "ff" (hexadecimal)
std::format("{:X}", 255); // "FF"
std::format("{:#x}", 255); // "0xff" (con prefijo)
std::format("{:08d}", 42); // "00000042" (ancho 8, relleno con 0)
std::format("{:+d}", 42); // "+42" (signo siempre)
// Flotantes
std::format("{:.2f}", 3.14159); // "3.14"
std::format("{:.4e}", 12345.6); // "1.2346e+04"
std::format("{:g}", 0.0001); // "0.0001" o "1e-04" según magnitud
// Alineación
std::format("{:>10}", "abc"); // " abc" (derecha)
std::format("{:<10}", "abc"); // "abc " (izquierda)
std::format("{:^10}", "abc"); // " abc " (centrado)
std::format("{:*^10}", "abc"); // "***abc****" (relleno personalizado)
Índices posicionales
// Reusar argumentos o cambiar el orden
std::format("{0} y {0} otra vez, luego {1}", "primero", "segundo");
// "primero y primero otra vez, luego segundo"
Formatear tipos propios
Para que std::format funcione con tus propios tipos, hay que especializar std::formatter<T>:
#include <format>
struct Punto { double x, y; };
template <>
struct std::formatter<Punto> {
// parse: analiza los especificadores entre { y }
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin(); // no aceptamos especificadores personalizados
}
// format: produce la salida
auto format(const Punto& p, std::format_context& ctx) const {
return std::format_to(ctx.out(), "({:.2f}, {:.2f})", p.x, p.y);
}
};
int main() {
Punto p{1.5, 3.7};
std::println("El punto es {}", p); // "El punto es (1.50, 3.70)"
}
Formatter con opciones
template <>
struct std::formatter<Punto> {
int precision = 2;
constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && *it != '}') {
// Leer precisión del especificador, p.ej. {:.4}
precision = *it++ - '0';
}
return it;
}
auto format(const Punto& p, std::format_context& ctx) const {
return std::format_to(ctx.out(),
"({:.{}f}, {:.{}f})", p.x, precision, p.y, precision);
}
};
// Uso: std::format("{:.4}", punto) -> "(1.5000, 3.7000)"
std::format_string: comprobación en tiempo de compilación
Una de las ventajas más importantes: la cadena de formato se valida en tiempo de compilación. Si los tipos no coinciden, el error aparece al compilar, no en ejecución:
// Error en compilación: {} espera argumento, se dan cero
// auto s = std::format("{}"); // error de compilación
// Error en compilación: tipo incorrecto
// auto s = std::format("{:d}", 3.14); // error: :d es para enteros
Rendimiento vs printf
La librería {fmt} (en la que se basa el estándar) tiene benchmarks que la ponen a la par o por encima de printf en la mayoría de casos. std::format tiene algo más de overhead que {fmt} directamente porque el estándar requiere localización, pero en la práctica el rendimiento es muy competitivo.
Para logging de alto rendimiento donde cada microsegundo importa, vale la pena medir. En la mayoría de aplicaciones, la seguridad de tipos y la legibilidad compensan con creces cualquier diferencia marginal.
Disponibilidad
std::format está completo en GCC 13+, Clang 14+ y MSVC VS2019 16.10+. std::print y std::println de C++23 requieren GCC 14+ o Clang 17+. Si necesitas compatibilidad con compiladores más antiguos, la librería {fmt} (en la que se basa el estándar) tiene exactamente la misma API y soporta desde GCC 5.
El artículo sobre novedades de C++23 cubre más características del mismo estándar. Y si te interesa cómo todo esto se integra en un proyecto real desde cero, el siguiente artículo sobre CMake moderno cierra el ciclo.
Imagen: Pexels / Seraphfim Gallery
