Respuesta a la pregunta.
Historia de la pregunta
El manejo de errores en C++ tradicionalmente se basaba en excepciones o códigos de error. Las excepciones proporcionaban una sintaxis limpia, pero incurrían en costos de tiempo de ejecución y eran difíciles de usar en contextos deterministas como sistemas embebidos o comercio en tiempo real. Los códigos de error eran eficientes pero contaminaban las firmas de las funciones y requerían comprobación manual de propagación. C++23 introdujo std::expected, un tipo de vocabulario que representa un valor o un error, inspirado en monadas de programación funcional como Either de Haskell o Result de Rust.
El problema
Aunque std::expected proporciona operaciones monádicas como and_then, or_else y transform, estas operaciones requieren un manejo explícito del tipo de error en cada paso de la cadena de composición. A diferencia del manejo basado en excepciones donde los errores se propagan automáticamente hacia arriba en la pila de llamadas hasta ser atrapados, std::expected requiere que el programador especifique explícitamente cómo los errores se transforman o propagan a través de cada enlace monádico. Esta explicitud crea código verboso al encadenar múltiples operaciones que podrían fallar, y requiere una consideración cuidadosa de las conversiones de tipos de error cuando diferentes operaciones devuelven diferentes tipos de error. El problema fundamental es que el sistema de tipos de C++ requiere una unificación explícita de tipos de error en las instancias de plantilla, a diferencia del manejo de excepciones dinámicas.
La solución
La interfaz monádica de std::expected de C++23 utiliza maquinaria de plantilla explícita para asegurar la seguridad de tipos y la abstracción sin sobrecarga. El método and_then requiere que el callable devuelva otro std::expected con tipos de error potencialmente diferentes, y la implementación utiliza SFINAE o conceptos para validar la composición. Para la propagación del tipo de error, los desarrolladores deben manejar explícitamente las conversiones de tipos utilizando or_else o mapear tipos de error usando transform_error. Este enfoque explícito asegura que las rutas de manejo de errores sean visibles en el código fuente y optimizables por el compilador, a diferencia del flujo de control de excepciones ocultas. La solución abraza principios de programación funcional mientras respeta la filosofía de cero sobrecarga de C++.
#include <expected> #include <string> #include <system_error> std::expected<int, std::error_code> parse_int(const std::string& s); std::expected<double, std::error_code> divide(int a, int b); // Manejo de errores explícito en la composición auto result = parse_int("42") .and_then([](int n) { return divide(100, n); }) .or_else([](std::error_code e) { return std::expected<double, std::error_code>(0.0); });
Situación de la vida real
Un equipo de software de dispositivos médicos necesitaba implementar una tubería de datos que procesara lecturas de sensores con múltiples etapas de validación. Cada etapa podría fallar con códigos de error específicos (tiempo de espera de hardware, falla de suma de verificación, error de calibración) que necesitaban propagarse al sistema de registro con completa seguridad de tipo.
El primer enfoque considerado fue el manejo de errores basado en excepciones utilizando jerarquías de std::runtime_error. Esto permitía la propagación automática hacia arriba en la pila de llamadas y una separación limpia del manejo de errores de la lógica de negocio. Sin embargo, los dispositivos médicos requerían garantías de latencia determinista, y las excepciones introducían una sobrecarga impredecible durante el desmantelamiento de la pila. El enfoque también hacía imposible usar el código en núcleos de GPU o contextos embebidos donde las excepciones estaban desactivadas. El equipo necesitaba una solución que funcionara en entornos noexcept.
El segundo enfoque considerado fue el uso tradicional de códigos de error utilizando std::optional o std::variant con comprobación manual de errores después de cada operación. Esto proporcionaba el determinismo requerido y la compatibilidad con noexcept. Sin embargo, el código se volvió desordenado con repetitivas comprobaciones if (!result) después de cada etapa de la tubería. La propagación de errores requería un enhebrado manual de códigos de error a través de la pila de llamadas, y componer múltiples operaciones requería condicionales anidados que oscurecían la lógica del flujo de datos. Los tipos de error también carecían de seguridad de tipo al mezclar diferentes categorías de error de varios sensores de hardware.
La solución elegida fue std::expected de C++23 con su interfaz monádica. El equipo refactorizó la tubería para usar and_then para encadenar pasos de validación y or_else para la transformación de errores. Esto preservó el flujo de datos lineal mientras mantenía rutas de manejo de errores explícitas. La solución proporcionó una abstracción sin sobrecarga compatible con las restricciones de noexcept y permitió una propagación precisa de tipos de error al sistema de registro. La refactorización tomó tres semanas, después de las cuales la base de código soportaba 15 tipos diferentes de sensores con manejo unificado de errores.
Lo que los candidatos a menudo pasan por alto
¿Cómo maneja std::expected la eliminación de tipos al encadenar operaciones que devuelven diferentes tipos de error?
Los candidatos a menudo pasan por alto que std::expected no realiza eliminación de tipos por defecto. Al usar and_then, el callable debe devolver un std::expected con el mismo tipo de error que el original, o el programa no compila.
Para manejar diferentes tipos de error, los desarrolladores deben transformar explícitamente los errores utilizando transform_error o usar std::expected con una variante de tipo de error común. A diferencia de las excepciones que utilizan un único tipo estático para todos los errores (generalmente std::exception_ptr o clases de excepciones base), std::expected mantiene seguridad de tipos estricta.
Este diseño previene costos ocultos de eliminación de tipos, pero requiere una unificación explícita de tipos de error en tiempo de compilación. Comprender esta distinción es crucial para componer operaciones de diferentes bibliotecas con categorías de error distintas.
¿Por qué std::expected no proporciona una operación bind monádica que propague automáticamente los errores como lo hace el manejo de excepciones?
Los candidatos con frecuencia confunden std::expected con el manejo de errores basado en excepciones en relación con la propagación automática. Esperan que si una operación en una cadena falla, las operaciones subsiguientes se salten automáticamente sin un manejo explícito.
Si bien and_then omite el callable en caso de error, el tipo de error aún debe manejarse explícitamente al final de la cadena o transformarse usando or_else. La razón fundamental es que el sistema de tipos de C++ requiere un manejo explícito de todos los posibles estados de error para mantener un comportamiento de cero sobrecarga y determinista.
La propagación automática requeriría un flujo de control implícito similar a las excepciones, lo que contradice el objetivo de diseño de rutas de errores explícitas y optimizables. Std::expected prioriza el rendimiento y el determinismo por encima de la conveniencia sintáctica.
¿Cómo afecta la especificación noexcept de las operaciones monádicas de std::expected a las garantías de seguridad de excepciones en las cadenas de composición?
Los candidatos a menudo pasan por alto que las operaciones monádicas de std::expected como and_then y transform son condicionalmente noexcept basadas en las operaciones que invocan. Si el callable pasado a and_then es noexcept, toda la cadena permanece noexcept.
Sin embargo, si el callable puede lanzar excepciones, la operación puede lanzar std::bad_expected_access o propagar la excepción dependiendo de la implementación específica y la estrategia de manejo de errores. Esta propagación noexcept condicional permite a los desarrolladores mantener fuertes garantías de seguridad de excepciones a lo largo de la cadena de composición.
Comprender esto es crucial para sistemas en tiempo real donde las especificaciones de excepciones afectan la generación de código y optimización. El contrato noexcept se propaga a través de la cadena monádica, asegurando que el manejo de errores permanezca determinista y optimizable por el compilador.