c++ variant c++17

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 (ver valueless_by_exception )
  • 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 ejemplo optional (vea la propuesta vinculada más arriba)
  • Cada uno proporciona una manera de verificar el vacío.
    • std::optional tiene una conversión implícita a bool , o alternativamente la función has_value
    • std::variant tiene valueless_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 es value y puede lanzar bad_optional_access
    • El acceso de lanzamiento potencial para std::variant es cppreference y puede lanzar bad_variant_access
    • No lanzar (uso el término un poco flojo) el acceso para std::optional es value_or que puede devolverle una alternativa (que usted pasa) si el optional está vacío
    • El acceso no std::get_if<T> para std::variant es std::get_if<T> que devuelve un valor nullptr si el índice o tipo proporcionado es incorrecto.

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.