c++ c++17 optional

c++ - std:: opcional-construir vacío con{} o std:: nullopt?



c++17 optional (4)

Pensé que inicializar un std::optional con std::nullopt sería lo mismo que la construcción predeterminada.

Se describen como iguales en cppreference , como form (1)

Sin embargo, tanto Clang como GCC parecen tratar estas funciones de ejemplo de juguete de manera diferente.

#include <optional> struct Data { char large_data[0x10000]; }; std::optional<Data> nullopt_init() { return std::nullopt; } std::optional<Data> default_init() { return {}; }

El Explorador de compiladores parece implicar que el uso de std::nullopt simplemente establecerá el indicador "contiene",

nullopt_init(): mov BYTE PTR [rdi+65536], 0 mov rax, rdi ret

Mientras que la construcción predeterminada valdrá la inicialización de toda la clase. Esto es funcionalmente equivalente, pero casi siempre es más costoso.

default_init(): sub rsp, 8 mov edx, 65537 xor esi, esi call memset add rsp, 8 ret

¿Es este comportamiento intencional? ¿Cuándo se debe preferir una forma sobre la otra?


Ejemplo motivacional

Cuando yo escribo:

std::optional<X*> opt{}; (*opt)->f();//expects error here, not UB or heap corruption

Esperaría que lo opcional se inicialice y no contenga memoria no inicializada. Además, no esperaría que la corrupción de un montón sea una consecuencia, ya que espero que todo se inicialice bien. Esto se compara con el puntero semántico de std::optional :

X* ptr{};//ptr is now zero ptr->f();//deterministic error here, not UB or heap corruption

Si escribo std::optional<X*>(std::nullopt) hubiera esperado lo mismo, pero al menos aquí parece más una situación ambigua.

La razón es la memoria no inicializada

Es muy probable que este comportamiento sea intencional .

(No soy parte de ningún comité, así que al final no puedo decir seguro)

Esta es la razón principal : una llave vacía init (zero-init) no debería conducir a una memoria no inicializada (aunque el lenguaje no impone esto como regla) - ¿de qué otra manera garantizará que no haya memoria no inicializada en su programa? ?

Para esta tarea, a menudo recurrimos al uso de herramientas de análisis estático: prominentemente la comprobación del núcleo de cpp que se basa en la aplicación de las directrices del núcleo de cpp ; en particular, hay algunas pautas sobre exactamente este tema. Si esto no hubiera sido posible, nuestro análisis estático fallaría para este caso aparentemente simple; o peor ser engañoso. Por el contrario, los contenedores basados ​​en el montón no tienen el mismo problema de forma natural.

Acceso no verificado

Recuerde que el acceso a std::optional no está marcado ; esto lleva al caso en el que podría acceder por error a esa memoria unitaria. Solo para mostrar esto, si ese no fuera el caso, entonces esto podría ser una gran corrupción:

std::optional<X*> opt{};//lets assume brace-init doesn''t zero-initialize the underlying object for a moment (in practice it does) (*opt)->f();//<- possible heap corruption

Sin embargo, con la implementación actual, esto se vuelve determinista (fallo de seguridad / violación de acceso en las plataformas principales).

Entonces podría preguntar, ¿por qué el std::nullopt ''especializado'' std::nullopt no inicializa la memoria?

No estoy realmente seguro de por qué no. Si bien supongo que no sería un problema si lo hiciera. En este caso, a diferencia del brace-init, no tiene el mismo tipo de expectativas. Sutilmente, ahora tienes una opción.

Para aquellos interesados, MSVC hace lo mismo.


El estándar no dice nada sobre la implementación de esos dos constructores. De acuerdo con [opcional.ctor] :

constexpr optional() noexcept; constexpr optional(nullopt_t) noexcept;

  1. Asegura: *this no contiene un valor.
  2. Observaciones: no se inicializa ningún valor contenido. Para cada tipo de objeto T estos constructores serán constructores constexpr (9.1.5).

Simplemente especifica la firma de esos dos constructores y sus "Garantías" (también conocidos como efectos): después de cualquiera de esas construcciones, lo optional no contiene ningún valor. No se otorgan otras garantías.

Si el primer constructor está definido por el usuario está definido por la implementación (es decir, depende del compilador).

Si el primer constructor está definido por el usuario, por supuesto, se puede implementar configurando el indicador contains . Pero un constructor no definido por el usuario también cumple con el estándar (implementado por gcc), porque esto también inicializa a cero el indicador a false . A pesar de que resulta en una inicialización costy zero, no viola las "Garantías" especificadas por el estándar.

En lo que respecta al uso en la vida real, bueno, es bueno que haya profundizado en las implementaciones para escribir un código óptimo.

Como nota al margen, probablemente el estándar debería especificar la complejidad de esos dos constructores (es decir, O(1) u O(sizeof(T)) )


En este caso, {} invoca la inicialización del valor. Si el constructor predeterminado de optional no es proporcionado por el usuario (donde "no proporcionado por el usuario" significa aproximadamente "está implícitamente declarado o explícitamente predeterminado en la definición de clase"), esto implica una inicialización cero del objeto completo.

Si lo hace, depende de los detalles de implementación de esa implementación std::optional . Parece que el constructor predeterminado de optional libstdc ++ no es proporcionado por el usuario, pero sí lo es de libc ++.


Para gcc, la reducción a cero innecesaria con inicialización predeterminada

std::optional<Data> default_init() { std::optional<Data> o; return o; }

es el error 86173 y debe corregirse en el compilador mismo. Usando el mismo libstdc ++, clang no realiza ningún memset aquí.

Ahora en su código, en realidad está inicializando el objeto (mediante la inicialización de la lista). Parece que las implementaciones de la biblioteca de std::optional tienen 2 opciones principales: o hacen que el constructor predeterminado sea trivial (libstdc ++), que tiene algunas ventajas pero obliga a la inicialización cero de todo el búfer; o proporcionan un constructor predeterminado (libc ++) que inicializa solo lo que se necesita (como el constructor de std::nullopt ), pero pierden la trivialidad. Lamentablemente, no parece posible tener las ventajas de ambos. Creo que prefiero la segunda versión. Mientras tanto, pragmáticamente, parece una buena idea usar el constructor de std::nullopt donde no complica el código.