Tema representativo de entrevista

Entrevista de C++: ¿cómo diseñarías un contrato de errores componible con std::expected?

CodingIntermedio
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Un servicio de pedidos puede fallar durante la validación, la reserva de inventario y el cobro. Diseña la cadena de propagación de errores con std::expected de C++23 y explica cuándo las excepciones aún corresponden a los límites.

Prompt y contexto

Un servicio de pedidos realiza la validación de entradas, la reserva de inventario y el cobro. La falta de stock, el pago rechazado y el tiempo de espera agotado de una dependencia son fallos esperados; los errores de programación, las invariantes rotas y los fallos de recursos irrecuperables necesitan una ruta separada. El entrevistador te pide diseñar los tipos de retorno, la composición y el registro de errores con std::expected de C++23.

Esto evalúa los contratos de error y el código componible. std::expected<T, E> contiene un valor T o un error E; no reintenta, no registra en logs ni revierte efectos secundarios por ti. Define primero el dominio de error, luego muestra cómo los llamadores inspeccionan los resultados y, finalmente, establece los límites de excepciones y compensación.

Qué evalúa el entrevistador

  • Si distingues entre fallo de negocio, fallo de dependencias e invariantes rotas.
  • Si utilizas correctamente has_value, value, error, unexpected y [[nodiscard]].
  • Si puedes componer fallos que no lanzan excepciones mediante and_then, transform y or_else.
  • Si evitas tratar std::expected como un reemplazo global de excepciones o como un error de cadena de texto sin formato.
  • Si cubres el mapeo de errores, la idempotencia, el registro (logging) y la compensación.

Preguntas para aclarar primero

  • ¿La base de código compila como C++23 y su biblioteca estándar implementa las API objetivo?
  • ¿El error se maneja localmente o se serializa a través del límite de un servicio? Los errores entre servicios necesitan códigos estables.
  • ¿Una función posee bloqueos (locks), descriptores de archivo o una reserva? Especifica la limpieza de estado y la compensación antes de devolver un fallo.
  • ¿Son idempotentes los pasos del pedido? Reintentar un cobro es diferente de liberar inventario dos veces.
  • ¿Qué campos de diagnóstico se deben conservar? Los mensajes para el usuario y los registros internos no deben mezclarse.

Un marco de respuesta de 30 segundos

“Modelaría los fallos de negocio esperados como std::expected<T, OrderError> para que los llamadores los manejen explícitamente. Marcaría los resultados como [[nodiscard]], compondría la validación, la reserva y el cobro con and_then, y usaría or_else para un mapeo y métricas consistentes. Las excepciones se reservan para invariantes rotas, fallos de inicialización o límites que no pueden recuperarse de manera segura. Cada efecto secundario necesita una clave de idempotencia, un plan de compensación y un registro sin datos confidenciales.”

Respuesta detallada

Definir el dominio del error

OrderError debe contener categorías sobre las que el llamador pueda actuar, tales como invalidrequest, outofstock, paymentdeclined y dependency_timeout, además de contexto interno controlado. No coloques el texto para el usuario, un stack trace y la carga útil del proveedor en una sola cadena. Los errores de programación y las invariantes rotas no deben convertirse silenciosamente en fallos de negocio ordinarios.

Elegir los tipos de valor y de error

La validación puede devolver std::expected<ValidatedOrder, OrderError>, la reserva puede devolver std::expected<Reservation, OrderError> y el cobro puede devolver std::expected<Receipt, OrderError>. Mantén el tipo de error movible, razonablemente pequeño y explícito en cuanto a la propiedad (ownership). Marca las funciones que producen resultados como [[nodiscard]] para que el llamador no pueda descartar un fallo de manera silenciosa.

Utilizar comprobaciones explícitas en límites claros

Cuando los pasos son cortos o tienen diferentes acciones por error, las comprobaciones explícitas son fáciles de auditar:

cpp
[[nodiscard]] auto reserve(const ValidatedOrder& order)
    -> std::expected<Reservation, OrderError>;

auto create_order(Input input) -> std::expected<Receipt, OrderError> {
  auto valid = validate(std::move(input));
  if (!valid) return std::unexpected(valid.error());

  auto held = reserve(*valid);
  if (!held) return std::unexpected(held.error());

  return charge(*valid, *held);
}

Usa operator* y value() solo después de confirmar el éxito. Conserva la categoría original en la ruta de fallo; no disfraces la falta de stock como un error genérico opaco.

Componer cadenas homogéneas con operaciones monádicas

Cuando cada paso devuelve std::expected, and_then continúa solo en caso de éxito y cortocircuita ante un error. transform mapea un valor exitoso, or_else registra o convierte un error, y C++23 también proporciona transform_error para la conversión en los límites. Mantén visible el orden de los efectos secundarios; no ocultes cobros, reintentos y compensaciones dentro de una cadena no auditable.

Establecer el límite de las excepciones

Los tiempos de espera agotados, la falta de stock y los pagos rechazados son resultados esperados, por lo que expected permite que la capa de negocio elija entre reintentar, notificar al usuario o realizar un manejo manual. Las invariantes rotas, los fallos de inicialización o un límite que no puede recuperarse de manera segura pueden usar excepciones. Mantén el límite consistente: una misma categoría de error no debería a veces retornarse y a veces lanzarse desde la misma función.

Manejar efectos secundarios, idempotencia y compensación

expected transporta un resultado; no deshace un efecto secundario completado. Si la reserva tiene éxito y el cobro falla, libera la reserva o pasa a un estado de compensación. Los reintentos de cobro necesitan una clave de idempotencia. Los objetos de error pueden contener el ID de pedido, el paso y consejos de reintento sin incluir secretos; el registro puede asociar una traza sin almacenar credenciales de pago.

Hacer que el contrato sea comprobable mediante pruebas

Prueba cada rama de error, cortocircuito, mapeo y orden de compensación. Comprueba que las excepciones inesperadas crucen el límite y se manejen de manera consistente. Fija en CI el compilador, las macros de características de la biblioteca estándar y las opciones de compilación. Entre servicios, mapea OrderError a códigos de protocolo estables en lugar de exponer los nombres de tipos de C++ como contratos de API.

Respuesta modelo de alta calidad

“Definiría OrderError con categorías estables y diagnósticos internos controlados. La validación, la reserva y el cobro devuelven valores [[nodiscard]] std::expected. La falta de stock, el pago rechazado y el tiempo de espera agotado son fallos de negocio manejados explícitamente por el llamador; no todos son excepciones.

Para una cadena corta usaría if (!result) y propagaría std::unexpected(result.error()), manteniendo auditable el límite de cada efecto secundario. Las transformaciones homogéneas puras pueden usar and_then y transform, mientras que or_else registra y mapea errores. Solo llamo a value() después de que se establece el éxito.

Si la reserva tiene éxito y el cobro falla, expected no lo revierte, por lo que registro un estado idempotente y ejecuto la liberación o compensación. Las excepciones se reservan para invariantes rotas y fallos de inicialización de los cuales el límite actual no puede recuperarse de forma segura. En CI se fija la cadena de herramientas de C++23 y se prueba cada ruta de error, cortocircuito y compensación antes de mapear los errores a códigos de servicio estables.”

Errores comunes

  • Tratar std::expected como un motor de reintentos: el tipo de retorno no tiene semántica de reintento → decide en la capa de negocio usando categorías de error e idempotencia.
  • Usar std::optional y perder el motivo: los llamadores no pueden distinguir la falta de stock de un tiempo de espera agotado → usa un tipo de error estable.
  • Ignorar [[nodiscard]]: los llamadores pueden descartar un fallo → anota las funciones de resultado y eleva las advertencias a errores en CI.
  • Llamar a value() sin comprobar: un fallo puede lanzar bad_expected_access → bifurca explícitamente o comprueba primero.
  • Convertir cada excepción en un código de error: las invariantes rotas pueden quedar ocultadas → mantén un límite de excepciones claro.
  • Incluir secretos en un objeto de error: los registros o la serialización pueden filtrar credenciales → separa los textos para el usuario, los códigos estables y los diagnósticos internos.
  • Ignorar los efectos secundarios completados: el inventario permanece reservado tras un cobro fallido → diseña un estado idempotente y mecanismos de compensación.
  • Enviar tipos de C++ a través de servicios: las actualizaciones rompen el protocolo → mapea a códigos y campos versionados y estables.

Preguntas de seguimiento y respuestas

Pregunta de seguimiento 1: ¿Cómo eliges entre std::expected y excepciones?

Usa expected para resultados de negocio esperados que el llamador pueda manejar. Usa excepciones para invariantes rotas, fallos de inicialización o un límite que no pueda recuperarse de forma segura. La consistencia importa más que una regla universal: los llamadores no deberían tener que adivinar el flujo de control para una misma categoría de error.

Pregunta de seguimiento 2: ¿Por qué no std::optional<T>?

optional representa presencia o ausencia, pero no lleva ningún motivo. El flujo de pedidos necesita acciones diferentes para fallos de inventario, pago y dependencias, por lo que requiere expected<T, E>.

Pregunta de seguimiento 3: ¿Puede ejecutarse el cobro dentro de and_then?

Puede hacerlo, siempre que los límites de los efectos secundarios permanezcan visibles y cada paso sea reintentable o compensable. Para una cadena larga con diferentes políticas, un if explícito puede ser más fácil de auditar; no ocultes cambios de estado solo por adoptar un estilo funcional.

Pregunta de seguimiento 4: ¿Cómo unificas los errores de varias dependencias?

Define un error de dominio estable en el límite del servicio. Mapea los fallos de proveedores a un conjunto finito de categorías mientras conservas una causa interna. Usa transform_error o or_else para el mapeo, registra internamente los códigos y trazas de proveedores, y mantén la redacción del proveedor fuera de los mensajes para el usuario.

Pregunta de seguimiento 5: ¿Devolver un error es más lento que lanzar una excepción?

Mide las rutas reales. expected hace explícitos los fallos comunes, pero puede copiar un objeto de error o añadir bifurcaciones; las excepciones concentran el costo en la ruta en la que se lanzan. Decide a partir de los objetivos de latencia, el comportamiento del compilador y pruebas de rendimiento de frecuencia de errores, no de una afirmación absoluta.

Pregunta de seguimiento 6: ¿Cómo evitas que los llamadores olviden comprobar los resultados?

Anota los tipos de resultado y las funciones importantes con [[nodiscard]], convierte las advertencias del compilador en fallos de CI y revisa cada llamada a value(). Para las API entre lenguajes, añade pruebas de estado de protocolo y de contrato para cubrir lo que el compilador no puede imponer.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta