c++ - ¿Cómo es std:: opcional nunca "sin valor por excepción"?
c++17 stdoptional (3)
std::variant
puede entrar en un estado llamado "
sin valor por excepción
".
Según tengo entendido, la causa común de esto es si una asignación de movimiento arroja una excepción. Ya no se garantiza que el valor anterior de la variante esté presente, ni tampoco el nuevo valor previsto.
std::optional
, sin embargo, no tiene ese estado.
cppreference hace la afirmación en negrita:
Si se produce una excepción, el estado de inicialización de * this ... no cambia, es decir, si el objeto contenía un valor, todavía contiene un valor, y viceversa.
¿Cómo es
std::optional
capaz de evitar ser "sin valor por excepción", mientras que
std::variant
no lo es?
"sin valor por excepción" se refiere a un escenario específico donde necesita cambiar el tipo almacenado en la variante. Eso necesariamente requiere 1) destruir el valor anterior y luego 2) crear el nuevo en su lugar. Si 2) falla, no tiene forma de regresar (sin gastos indebidos inaceptables para el comité).
optional
no tiene este problema.
Si alguna operación en el objeto que contiene produce una excepción, que así sea.
El objeto sigue ahí.
Eso no significa que el estado del objeto siga siendo significativo, es lo que sea que la operación de lanzamiento lo deje. Esperemos que esa operación tenga al menos la garantía básica.
optional<T>
tiene uno de dos estados:
-
una
T
- vacío
Una
variant
solo puede ingresar al estado sin valor cuando se realiza la transición de un estado a otro si la transición arroja, porque de alguna manera necesita recuperar el objeto original y las diversas estrategias para hacerlo requieren almacenamiento adicional
1
, asignación de almacenamiento dinámico
2
o un estado vacío
3)
Pero para
optional
, la transición de
T
a vacío es solo una destrucción.
Entonces eso solo arroja si el destructor de
T
lanza, y realmente a quién le importa en ese punto.
Y la transición de vacío a
T
no es un problema; si eso arroja, es fácil recuperar el objeto original: el estado vacío está vacío.
El caso desafiante es:
emplace()
cuando ya teníamos una
T
Necesitamos necesariamente haber destruido el objeto original, entonces, ¿qué hacemos si la construcción del emplazamiento arroja?
Con
optional
, tenemos un estado vacío conveniente y conocido al que recurrir, por lo que el diseño es solo para hacer eso.
problemas de la
variant
de no tener ese estado fácil para recuperarse.
1
Como
boost::variant2
hace.
2
Como
boost::variant
hace.
3
No estoy seguro de una implementación de variante que haga esto, pero hubo una sugerencia de diseño de que la
variant<monostate, A, B>
podría pasar al estado de
monostate
si tuviera una
A
y la transición a
B
produjera.
std::optional
tiene fácil:
-
Contiene un valor y se le asigna un nuevo valor:
Fácil, simplemente delegue en el operador de asignación y deje que se ocupe de ello. Incluso en el caso de una excepción, todavía quedará un valor. -
Contiene un valor y el valor se elimina:
Fácil, el dtor no debe tirar. La biblioteca estándar generalmente supone que para los tipos definidos por el usuario. -
No contiene ningún valor y se le asigna uno:
Volver a ningún valor frente a una excepción en la construcción es bastante simple. -
No contiene ningún valor y no se le asigna ningún valor:
Trivial.
std::variant
tiene el mismo tiempo fácil cuando el tipo almacenado no cambia.
Desafortunadamente, cuando se le asigna un tipo diferente, debe dar lugar destruyendo el valor anterior y luego construir el nuevo valor.
Como el valor anterior ya está perdido, ¿qué puede hacer?
Márquelo
como
sin valor por excepción
para tener un estado estable, válido aunque indeseable, y deje que la excepción se propague.
Se podría usar espacio y tiempo extra para asignar los valores dinámicamente, guardar el valor antiguo en algún lugar temporalmente, construir el nuevo valor antes de asignarlo o similar, pero todas esas estrategias son costosas y solo la primera siempre funciona.