El pasado marzo de 2026, el comité ISO/IEC ratificó el estándar C++26 en la reunión celebrada en Croydon (Londres). Con esto acaba un ciclo de tres años en el que han entrado propuestas que llevaban mucho tiempo esperando: reflection en tiempo de compilación, un modelo formal de comportamiento para variables no inicializadas, contratos nativos y una nueva abstracción de concurrencia asíncrona. GCC 16 y Clang 20 ya implementan la mayoría de estas características, así que no son solo papel: se pueden usar hoy.
Reflection en tiempo de compilación
La propuesta P2996 introduce el operador ^^ para obtener una representación reflectiva de cualquier tipo o expresión. El resultado es un valor de tipo std::meta::info, un tipo opaco que el compilador maneja de forma nativa. Con él se puede inspeccionar una estructura, listar sus miembros o generar código en tiempo de compilación sin recurrir a macros ni a librerías externas de generación de código.
Un ejemplo práctico: serializar una struct a JSON sin escribir ningún código manual por campo:
#include <meta>
#include <string>
#include <format>
template <typename T>
std::string to_json(const T& obj) {
std::string result = "{";
bool first = true;
[:expand(std::meta::nonstatic_data_members_of(^^T)):] >> [&]<auto Member>() {
if (!first) result += ",";
result += std::format(""{}":{}",
std::meta::identifier_of(Member),
obj.[:Member:]);
first = false;
};
return result + "}";
}
struct Punto { int x; int y; };
int main() {
Punto p{3, 7};
// Imprime: {"x":3,"y":7}
// sin haber escrito ningún serializador manual
}
La sintaxis es nueva y requiere un poco de rodaje, pero lo que permite es potente: ORMs, motores de serialización, herramientas de testing basadas en introspección, todo sin tocar el preprocesador. La propuesta P3394 complementa esto con mejoras para trabajar con miembros de enumeraciones.
Memory safety: de Undefined Behavior a Erroneous Behavior
Una de las quejas más repetidas sobre C++ es que leer una variable no inicializada provoca Undefined Behavior, lo que en la práctica significa que el compilador puede hacer literalmente cualquier cosa: eliminar ramas de código, invertir condiciones, o simplemente producir resultados incorrectos sin aviso. C++26 cambia esto con la propuesta P2795.
La categoría nueva se llama erroneous behavior: el comportamiento está definido (el compilador no puede hacer cosas arbitrarias), pero se considera un error del programador y es diagnosticable. En concreto, leer un entero no inicializado produce un valor no especificado, pero no rompe el modelo de ejecución del programa. Los compiladores pueden insertar diagnósticos, y las herramientas de análisis estático tienen una base formal sobre la que trabajar.
void ejemplo() {
int x; // C++23: UB si se lee x
// C++26: erroneous behavior, valor indeterminado
int y = x + 1; // Definido, pero incorrecto; el compilador puede avisar
}
Esto no elimina los punteros crudos ni convierte C++ en un lenguaje con memoria gestionada, pero sí reduce la categoría de errores silenciosos que producen fallos difíciles de reproducir. Es una respuesta parcial a las presiones de organismos como la NSA y CISA, que llevan años pidiendo a la industria que abandone los lenguajes con gestión manual de memoria para software crítico. Si te interesa cómo Rust aborda este mismo problema desde otra perspectiva, puedes leer sobre ownership y borrowing en Rust.
Contratos nativos
C++26 incluye la propuesta P2900, que añade precondiciones, postcondiciones y aserciones de contrato directamente en la sintaxis del lenguaje, sin depender de macros como assert() o de librerías externas.
#include <cmath>
double raiz_cuadrada(double x)
pre(x >= 0.0)
post(resultado: resultado >= 0.0)
{
return std::sqrt(x);
}
void procesar(int* ptr, int n)
pre(ptr != nullptr)
pre(n > 0)
{
for (int i = 0; i < n; ++i) {
contract_assert(ptr[i] >= 0);
// ...
}
}
La diferencia respecto a assert() es que los contratos son parte del sistema de tipos y de la semántica del compilador. Se pueden configurar para distintos modos: ignorarlos en producción, lanzar excepciones al violarse, o terminar el programa. Además, un compilador que ve un contrato puede usarlo como información para optimizar, igual que hace con [[likely]] o con los invariantes que deduce del análisis de flujo.
std::execution: concurrencia asíncrona unificada
C++20 trajo corrutinas, pero sin ninguna abstracción de alto nivel incluida en la biblioteca estándar. Cada framework (ASIO, libunifex, stdexec, CUDA) definía sus propias primitivas, y el código que combinaba dos de ellos era un dolor. La propuesta P2300 entra en C++26 con el objetivo de resolver eso.
El modelo se basa en senders y receivers. Un sender describe una operación asíncrona; un receiver describe qué hacer con el resultado. Las dos piezas se conectan mediante adaptadores:
#include <execution>
#include <print>
namespace ex = std::execution;
int main() {
auto sched = ex::thread_pool_scheduler{};
auto trabajo = ex::schedule(sched)
| ex::then([]{ return 42; })
| ex::then([](int x){ return x * 2; });
auto [resultado] = ex::sync_wait(std::move(trabajo)).value();
std::println("Resultado: {}", resultado); // 84
}
La parte interesante es que los senders son componibles: ex::when_all permite esperar varios trabajos en paralelo, y el mismo código funciona sobre un pool de hilos, sobre CUDA o sobre un scheduler de tiempo real, con solo cambiar el objeto scheduler. No hay que reescribir la lógica.
Esto cambia bastante la forma de escribir código asíncrono en C++. Si vienes de Rust y conoces su modelo de async/await, la comparación no es directa, pero los objetivos son similares: composición sin callbacks, sin inversión de control forzada, y sin acoplamiento al runtime. Puedes ver cómo Rust 2024 Edition avanza en esa dirección en el artículo sobre Rust 2024 Edition.
Otras novedades que merecen atención
std::inplace_vector
std::inplace_vector<T, N> es un vector de capacidad fija que vive en el stack. Sin heap, sin alojamiento dinámico, con la misma interfaz que std::vector. Muy útil en entornos embebidos o en rutas de código donde la alojación dinámica es inaceptable por latencia o por restricciones de seguridad.
#include <inplace_vector> std::inplace_vector<int, 8> v; v.push_back(1); v.push_back(2); // Máximo 8 elementos; push_back lanza si se supera
#embed
C23 ya incluía #embed para incrustar datos binarios directamente en el ejecutable sin tener que convertirlos a arrays de bytes a mano. C++26 adopta la misma directiva. Incrustar una imagen, un shader o un certificado queda así:
constexpr unsigned char icono[] = {
#embed "icono.png"
};
Mucho más limpio que los scripts de conversión a hexadecimal que se usaban antes, y sin depender de herramientas externas en el build system.
constexpr ampliado
Más algoritmos de <algorithm> y <numeric> son ahora constexpr, lo que permite usarlos en contextos de tiempo de compilación sin restricciones adicionales. El compilador puede evaluar en compile time operaciones que antes requerían código manual con bucles constexpr.
Estado de los compiladores
GCC 16 implementa reflection (P2996) de forma experimental, contratos (P2900) y la mayoría de las propuestas de biblioteca. Clang 20 tiene soporte parcial de reflection y contratos, y avanza rápido. MSVC sigue su ritmo habitual: más lento, pero constante. Para proyectos que ya usan C++20 con corrutinas o módulos, la transición a C++26 no debería ser traumática: las propuestas nuevas son aditivas y no rompen compatibilidad hacia atrás.
Si te interesa comparar con otras alternativas modernas para programación de sistemas, Zig 0.14 ofrece un enfoque diferente al problema de la seguridad de memoria sin adoptar el modelo de ownership de Rust ni la complejidad de C++.
Imagen: Pexels / Nemuel Sereti
