c++ - ¿Por qué std:: get para variantes arroja en caso de falla en lugar de ser un comportamiento indefinido?
c++17 (4)
De acuerdo con cppreference std::get
para variant
std::bad_variant_access
si el tipo contenido en la variant
no es el esperado. Esto significa que la biblioteca estándar debe verificar cada acceso (libc++) .
¿Cuál fue la razón de esta decisión? ¿Por qué no es un comportamiento indefinido, como en todas partes en C ++? ¿Puedo evitarlo?
¿Cuál fue la razón de esta decisión?
Este tipo de pregunta siempre es difícil de responder, pero lo intentaré.
Gran parte de la inspiración para el comportamiento de std::variant
proviene del comportamiento de std::optional
, como se indica en la propuesta para std::variant
, P0088 :
Esta propuesta intenta aplicar las lecciones aprendidas de
optional
...
Y puedes ver los paralelos entre los dos tipos:
- No estás seguro de lo que está siendo retenido actualmente.
- en
optional
es un tipo o nada (nullopt_t
) - en la
variant
es uno de los muchos tipos, o nada (vervalueless_by_exception
)
- en
- Todas las funciones para operar en el tipo están marcadas como
constexpr
- Esto puede parecer una coincidencia o solo buenas prácticas de diseño, pero estaba claramente pensado que la
variant
siga el ejemplooptional
(vea la propuesta vinculada más arriba)
- Esto puede parecer una coincidencia o solo buenas prácticas de diseño, pero estaba claramente pensado que la
- Cada uno proporciona una manera de verificar el vacío.
-
std::optional
tiene una conversión implícita abool
, o alternativamente la funciónhas_value
-
std::variant
tienevalueless_by_exception
que le dice si la variante está vacía porque la construcción del tipo activo arrojó una excepción
-
- Cada uno de ellos proporciona un medio para un acceso de lanzamiento y no de lanzamiento.
- El acceso de lanzamiento potencial para
std::optional
esvalue
y puede lanzarbad_optional_access
- El acceso de lanzamiento potencial para
std::variant
es cppreference y puede lanzarbad_variant_access
- No lanzar (uso el término un poco flojo) el acceso para
std::optional
esvalue_or
que puede devolverle una alternativa (que usted pasa) si eloptional
está vacío - El acceso no std::get_if<T> para
std::variant
es std::get_if<T> que devuelve un valornullptr
si el índice o tipo proporcionado es incorrecto.
- El acceso de lanzamiento potencial para
De hecho, las similitudes fueron tan intencionadas, que una incoherencia en las clases base utilizadas para la optional
y la variant
fueron motivo de queja (consulte esta discusión de Grupos de Google )
Así que para responder a tu pregunta, se tira porque los tiros optional
. Tenga en cuenta que el comportamiento de lanzamiento rara vez se debe encontrar; debe usar un patrón de visitante con una variante, e incluso si llama, solo se get
tiros si le proporciona un índice del tamaño de la lista de tipos, o el tipo solicitado no es el activo. Todos los demás usos indebidos se consideran mal formados y deberían emitir un error de compilación.
En cuanto a por qué std::optional
throws, si comprueba su propuesta, N3793 con un N3793 al lanzamiento se promocionó como una mejora sobre Boost.Optional , de la que nace std::optional
. Todavía no he encontrado ninguna discusión acerca de por qué esto es una mejora, así que por ahora especularé: fue fácil proporcionar accesores de lanzamiento y no de lanzamiento que satisfacen ambos campos de manejo de errores (valores de centinela vs excepciones), y además, ayuda a eliminar un poco del comportamiento indefinido del lenguaje para que no se dispare innecesariamente a sí mismo en el pie si elige ir a la ruta de lanzamiento potencial.
¿Por qué no es un comportamiento indefinido, como en cualquier otro lugar en c ++? ¿Puedo evitarlo?
Sí, hay una solución directa. Si no desea seguridad de tipo, use una union
simple en lugar de una std::variant
. Como se dice en la referencia citada:
La plantilla de clase std :: variant representa una unión segura para el tipo.
El propósito de la union
era tener un solo objeto que pudiera tomar valores de uno de varios tipos diferentes. Solo un tipo de union
era "válido" en un momento dado, dependiendo de las variables miembro que se habían asignado:
union example {
int i;
float f;
};
// code block later...
example e;
e.i = 10;
std::cout << e.f << std::endl; // will compile but the output is undefined!
std::variant
generalizó una union
al agregar seguridad de tipos para asegurarse de que solo está accediendo al tipo de datos correcto. Si no desea esta seguridad, siempre puede utilizar un union
.
¿Qué fue lo racional para esta decisión?
No sé personalmente cuál fue el fundamento de esta decisión, pero siempre puede echar un vistazo a los documentos del comité de estandarización de C ++ para obtener información sobre el proceso.
Creo que lo encontré!
Parece que la razón se puede encontrar bajo "Diferencias con la revisión 5" en la proposal :
El compromiso de Kona: f! V.valid (), make get <...> (v) y visita (v) throw.
Es decir, que la variante debe incluir el estado "values_by_exception". Usando lo mismo if
siempre podemos tirar.
Incluso sabiendo esto racional, personalmente me gustaría evitar esta comprobación. El *get_if
de la respuesta de Justin me parece muy bueno (al menos para el código de la biblioteca).
La API actual para std::variant
no tiene una versión sin std::get
de std::get
. No sé por qué se estandarizó de esa manera; Todo lo que diga sería simplemente adivinar.
Sin embargo, puede acercarse al comportamiento deseado escribiendo *std::get_if<T>(&variant)
. Si la variant
no contiene T
en ese momento, std::get_if<T>
devuelve nullptr
, por lo tanto, al nullptr
referencia se trata de un comportamiento indefinido. El compilador puede así asumir que la variante contiene T
En la práctica, esta no es la optimización más fácil de hacer para el compilador. En comparación con una unión etiquetada simple, el código que emite puede no ser tan bueno. El siguiente código:
int const& get_int(std::variant<int, std::string> const& variant)
{
return *std::get_if<int>(&variant);
}
Emite esto con un sonido 5.0.0 :
get_int(std::variant<int, std::string> const&):
xor eax, eax
cmp dword ptr [rdi + 24], 0
cmove rax, rdi
ret
Está comparando el índice de la variante y moviendo condicionalmente el valor de retorno cuando el índice es correcto. Aunque sería UB si el índice fuera incorrecto, actualmente Clang no puede optimizar la comparación.
Curiosamente, devolver un int
lugar de una referencia optimiza la comprobación :
int get_int(std::variant<int, std::string> const& variant)
{
return *std::get_if<int>(&variant);
}
Emite:
get_int(std::variant<int, std::string> const&):
mov eax, dword ptr [rdi]
ret
Podría ayudar al compilador utilizando __builtin_unreachable()
o __assume
, pero actualmente gcc es el único compilador capaz de eliminar las comprobaciones cuando lo hace.