c++ - any - std optional




std:: opcional-construa vazio com{} ou std:: nullopt? (3)

Exemplo motivacional

Quando escrevo:

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

Eu esperaria que o opcional seja inicializado e não contenha memória não inicializada. Também não esperaria que uma corrupção de pilha fosse uma consequência, já que estou esperando que tudo seja inicializado corretamente. Isso se compara à semântica do ponteiro de std::optional :

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

Se eu escrever std::optional<X*>(std::nullopt) eu esperava o mesmo, mas pelo menos aqui parece mais uma situação ambígua.

O motivo é a memória não inicializada

É muito provável que esse comportamento seja intencional .

(Não sou parte de nenhum comitê, por isso, no final, não posso ter certeza)

Este é o principal motivo : um init de chave vazia (zero-init) não deve levar à memória não inicializada (embora o idioma não imponha isso como regra) - de que outra forma você garantirá que não há memória não inicializada no seu programa ?

Para esta tarefa, geralmente recorremos ao uso de ferramentas de análise estática: destaque no cpp core check, baseado na aplicação das diretrizes do cpp core ; em particular, existem algumas diretrizes sobre exatamente esse problema. Se isso não fosse possível, nossa análise estática falharia nesse caso aparentemente simples; ou pior, ser enganoso. Por outro lado, os contêineres baseados em heap não têm o mesmo problema naturalmente.

Acesso não verificado

Lembre-se de que o acesso ao std::optional está desmarcado - isso leva ao caso em que você pode, por engano, acessar essa memória unitizada. Apenas para mostrar isso, se não fosse esse o caso, isso poderia ser corrupção de pilha:

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

Com a implementação atual, no entanto, isso se torna determinístico (falha de seg / violação de acesso nas principais plataformas).

Então você pode perguntar: por que o std::nullopt 'specialized' não inicializa a memória?

Não sei ao certo por que não. Embora eu acho que não seria um problema se isso acontecesse. Nesse caso, ao contrário do brace-init, ele não vem com o mesmo tipo de expectativas. Sutilmente, agora você tem uma escolha.

Para os interessados, a MSVC faz o mesmo.

Eu pensei que inicializar um std::optional com std::nullopt seria o mesmo que a construção padrão.

Eles são descritos da mesma forma na cppreference , como forma (1)

No entanto, Clang e GCC parecem tratar essas funções de exemplo de brinquedos de maneira diferente.

#include <optional>

struct Data {
    char large_data[0x10000];
};

std::optional<Data> nullopt_init() {
  return std::nullopt;
}

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

O Compiler Explorer parece sugerir que o uso de std::nullopt simplesmente definirá o sinalizador "contém",

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

Enquanto a construção padrão valorizará a inicialização de toda a classe. Isso é funcionalmente equivalente, mas quase sempre mais caro.

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

Esse é um comportamento intencional? Quando uma forma deve ser preferida à outra?


Nesse caso, {} chama a inicialização de valor. Se o construtor padrão do optional não for fornecido pelo usuário (onde "não fornecido pelo usuário" significa aproximadamente "é declarado implicitamente ou explicitamente padronizado na definição de classe"), isso implica na inicialização zero de todo o objeto.

Se isso acontecer, depende dos detalhes da implementação dessa implementação std::optional . Parece que o construtor padrão do optional libstdc ++ não é fornecido pelo usuário, mas sim o libc ++.


Para gcc, o zeramento desnecessário com inicialização padrão

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

é o erro 86173 e precisa ser corrigido no próprio compilador. Usando o mesmo libstdc ++, o clang não executa nenhum memset aqui.

Agora, no seu código, você está realmente inicializando o objeto com valor (através da inicialização da lista). Parece que as implementações de biblioteca de std::optional têm 2 opções principais: ou tornam o construtor padrão trivial (libstdc ++), o que tem algumas vantagens, mas força a inicialização zero de todo o buffer; ou eles fornecem um construtor padrão (libc ++) que inicializa apenas o necessário (como o construtor de std::nullopt ), mas perde a trivialidade. Infelizmente, não parece possível ter as vantagens de ambos. Eu acho que prefiro a segunda versão. Enquanto isso, pragmaticamente, usar o construtor de std::nullopt onde não complica o código, parece ser uma boa idéia.







optional