Como se deve usar std :: optional?

133

Estou lendo a documentação std::experimental::optionale tenho uma boa idéia sobre o que faz, mas não entendo quando devo usá-lo ou como devo usá-lo. O site ainda não contém exemplos, o que me dificulta a compreensão do verdadeiro conceito desse objeto. Quando é std::optionaluma boa escolha para usar e como compensa o que não foi encontrado no Padrão anterior (C ++ 11).

0x499602D2
fonte
19
Os documentos opcionais do boost.op podem lançar alguma luz.
Juanchopanza
Parece que std :: unique_ptr geralmente pode servir os mesmos casos de uso. Eu acho que se você tem algo contra o novo, o opcional pode ser preferível, mas me parece que (desenvolvedores | aplicativos) com esse tipo de visão no novo estão em uma pequena minoria ... AFAICT, opcional NÃO é uma ótima idéia. No mínimo, a maioria de nós poderia viver confortavelmente sem ele. Pessoalmente, eu ficaria mais confortável em um mundo em que não tenho que escolher entre unique_ptr e opcional. Pode me chamar de louco, mas o Zen do Python está certo: deixe que haja uma maneira certa de fazer alguma coisa!
Allyourcode 4/15
19
Nem sempre queremos alocar algo no heap que precisa ser excluído; portanto, unique_ptr não substitui o opcional.
Krum
5
@allyourcode Nenhum ponteiro é um substituto para optional. Imagine que você quer um optional<int>ou até mesmo <char>. Você realmente acha que é necessário que o "Zen" aloque, desreferencie e exclua dinamicamente - algo que de outra forma não exigiria nenhuma alocação e caberia de forma compacta na pilha, e possivelmente até em um registro?
underscore_d
1
Outra diferença, que resulta sem dúvida da alocação de pilha versus heap do valor contido, é que o argumento do modelo não pode ser um tipo incompleto no caso de std::optional(embora possa ser std::unique_ptr). Mais precisamente, a norma exige que T [...] atenda aos requisitos de Destrutível .
dan_din_pantelimon

Respostas:

172

O exemplo mais simples que consigo pensar:

std::optional<int> try_parse_int(std::string s)
{
    //try to parse an int from the given string,
    //and return "nothing" if you fail
}

A mesma coisa pode ser realizada com um argumento de referência (como na assinatura a seguir), mas o uso std::optionaltorna a assinatura e o uso mais agradáveis.

bool try_parse_int(std::string s, int& i);

Outra maneira de fazer isso é especialmente ruim :

int* try_parse_int(std::string s); //return nullptr if fail

Isso requer alocação dinâmica de memória, preocupação com propriedade etc. - sempre prefira uma das outras duas assinaturas acima.


Outro exemplo:

class Contact
{
    std::optional<std::string> home_phone;
    std::optional<std::string> work_phone;
    std::optional<std::string> mobile_phone;
};

É extremamente preferível ter algo como a std::unique_ptr<std::string>para cada número de telefone! std::optionalfornece a localização dos dados, o que é ótimo para o desempenho.


Outro exemplo:

template<typename Key, typename Value>
class Lookup
{
    std::optional<Value> get(Key key);
};

Se a pesquisa não tiver uma determinada chave, podemos simplesmente retornar "sem valor".

Eu posso usá-lo assim:

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Outro exemplo:

std::vector<std::pair<std::string, double>> search(
    std::string query,
    std::optional<int> max_count,
    std::optional<double> min_match_score);

Isso faz muito mais sentido do que, digamos, ter quatro sobrecargas de função que usam todas as combinações possíveis de max_count(ou não) e min_match_score(ou não)!

Ele também elimina o maldito "passe -1para max_countse você não quiser um limite" ou "passe std::numeric_limits<double>::min()para min_match_scorese você não quiser uma pontuação mínima"!


Outro exemplo:

std::optional<int> find_in_string(std::string s, std::string query);

Se a string de consulta não estiver inserida s, desejo "não int" - não qualquer valor especial que alguém decida usar para esse fim (-1?).


Para exemplos adicionais, você pode consultar a boost::optional documentação . boost::optionale std::optionalserá basicamente idêntico em termos de comportamento e uso.

Timothy Shields
fonte
13
@gnzlbg std::optional<T>é apenas um Te um bool. As implementações da função membro são extremamente simples. O desempenho não deve ser realmente uma preocupação ao usá-lo - há momentos em que algo é opcional; nesse caso, essa é frequentemente a ferramenta correta para o trabalho.
Timothy Shields
8
@ TimothyShields std::optional<T>é muito mais complexo que isso. Ele usa posicionamento newe muito mais outras coisas com alinhamento e tamanho adequados para torná-lo um tipo literal (ou seja, usar com constexpr) entre outras coisas. A ingênua Te a boolabordagem falhariam rapidamente.
Rapptz 13/03/14
15
@ Rapptz Linha 256: union storage_t { unsigned char dummy_; T value_; ... }Linha 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Como isso não é "a Te a bool"? Concordo plenamente que a implementação é muito complicada e não trivial, mas conceitualmente e concretamente o tipo é a Te a bool. "A ingênua Te a boolabordagem falhariam rapidamente". Como você pode fazer essa declaração ao olhar para o código?
Timothy Shields
12
O @Rapptz ainda está armazenando um bool e o espaço para um int. A união está lá apenas para fazer com que o opcional não construa um T se não for realmente desejado. Ainda é struct{bool,maybe_t<T>}o sindicato que está lá para não fazer o struct{bool,T}que construiria um T em todos os casos.
PeterT
12
@allyourcode Muito boa pergunta. Ambos std::unique_ptr<T>e std::optional<T>, em certo sentido, cumprem o papel de "um opcional T". Eu descreveria a diferença entre eles como "detalhes de implementação": alocações extras, gerenciamento de memória, localidade dos dados, custo de movimentação, etc. Eu nunca teria, std::unique_ptr<int> try_parse_int(std::string s);por exemplo, porque causaria uma alocação para todas as chamadas, mesmo que não haja motivo para . Eu nunca teria uma classe com um std::unique_ptr<double> limit;- por que fazer uma alocação e perder a localidade dos dados?
Timothy Shields
35

Um exemplo é citado em Novo artigo adotado: N3672, std :: opcional :

 optional<int> str2int(string);    // converts int to string if possible

int get_int_from_user()
{
     string s;

     for (;;) {
         cin >> s;
         optional<int> o = str2int(s); // 'o' may or may not contain an int
         if (o) {                      // does optional contain a value?
            return *o;                  // use the value
         }
     }
}
taocp
fonte
13
Como você pode passar as informações sobre se você tem uma inthierarquia de chamada para cima ou para baixo, em vez de passar algum valor "fantasma" "assumido" com o significado de "erro".
Luis Machuca
1
@ Wiz Este é realmente um ótimo exemplo. (A) permite str2int()implementar a conversão da maneira que desejar, (B) independentemente do método de obtenção da string s, e (C) transmite o significado completo por meio optional<int>de uma maneira estúpida de número mágico, bool/ referência ou alocação dinâmica. Fazendo.
Sublinhado #
10

mas não entendo quando devo usá-lo ou como devo usá-lo.

Considere quando você está escrevendo uma API e deseja expressar que o valor "sem retorno" não é um erro. Por exemplo, você precisa ler dados de um soquete e, quando um bloco de dados estiver completo, analise-o e devolva-o:

class YourBlock { /* block header, format, whatever else */ };

std::optional<YourBlock> cache_and_get_block(
    some_socket_object& socket);

Se os dados anexados concluíram um bloco analisável, você pode processá-lo; caso contrário, continue lendo e anexando dados:

void your_client_code(some_socket_object& socket)
{
    char raw_data[1024]; // max 1024 bytes of raw data (for example)
    while(socket.read(raw_data, 1024))
    {
        if(auto block = cache_and_get_block(raw_data))
        {
            // process *block here
            // then return or break
        }
        // else [ no error; just keep reading and appending ]
    }
}

Editar: sobre o restante de suas perguntas:

Quando std :: optional é uma boa opção para usar

  • Quando você calcula um valor e precisa devolvê-lo, torna-se melhor a semântica retornar por valor do que fazer referência a um valor de saída (que pode não ser gerado).

  • Quando você deseja garantir que o código do cliente tenha que verificar o valor de saída (quem escreve o código do cliente pode não verificar se há erro - se você tentar usar um ponteiro não inicializado, receberá um dump principal; se você tentar usar um inicializado std :: opcional, você recebe uma exceção que pode ser capturada).

[...] e como isso compensa o que não foi encontrado na Norma anterior (C ++ 11).

Antes do C ++ 11, era necessário usar uma interface diferente para "funções que podem não retornar um valor" - retorne pelo ponteiro e verifique NULL ou aceite um parâmetro de saída e retorne um código de erro / resultado para "não disponível "

Ambos impõem um esforço extra e atenção do implementador do cliente para acertar e ambos são uma fonte de confusão (o primeiro leva o implementador do cliente a pensar em uma operação como uma alocação e exige que o código do cliente implemente a lógica de manipulação de ponteiros e o segundo permite código do cliente para evitar o uso de valores inválidos / não inicializados).

std::optional cuida bem dos problemas que surgem com as soluções anteriores.

utnapistim
fonte
Eu sei que eles são basicamente os mesmos, mas por que você está usando em boost::vez de std::?
0x499602D2
4
Obrigado - eu o corrigi (usei boost::optionalporque, após cerca de dois anos de uso, ele é codificado no meu córtex pré-frontal).
Utnapistim
1
Isso me parece um péssimo exemplo, pois se vários blocos fossem concluídos, apenas um poderia ser retornado e o restante descartado. A função deve retornar uma coleção de blocos possivelmente vazia.
Ben Voigt
Você está certo; foi um péssimo exemplo; Modifiquei meu exemplo para "analisar primeiro bloco, se disponível".
Utnapistim
4

Costumo usar opcionais para representar dados opcionais extraídos de arquivos de configuração, ou seja, onde esses dados (como com um elemento esperado, ainda que não necessário) em um documento XML) são fornecidos opcionalmente, para que eu possa mostrar de forma explícita e clara se os dados estavam realmente presentes no documento XML. Especialmente quando os dados podem ter um estado "não definido", versus um estado "vazio" e "definido" (lógica difusa). Com um opcional, definir e não definir é desobstruído, também vazio seria desobstruído com o valor de 0 ou nulo.

Isso pode mostrar como o valor de "não definido" não é equivalente a "vazio". Em conceito, um ponteiro para um int (int * p) pode mostrar isso, onde um nulo (p == 0) não está definido, um valor de 0 (* p == 0) está definido e vazio e qualquer outro valor (* p <> 0) está definido como um valor.

Para um exemplo prático, eu tenho uma parte da geometria extraída de um documento XML que tinha um valor chamado sinalizadores de renderização, onde a geometria pode substituir os sinalizadores de renderização (conjunto), desativar os sinalizadores de renderização (definido como 0) ou simplesmente não afetar os sinalizadores de renderização (não definidos), uma opção opcional seria uma maneira clara de representar isso.

Claramente, um ponteiro para um int, neste exemplo, pode atingir o objetivo, ou melhor, um ponteiro de compartilhamento, pois pode oferecer uma implementação mais limpa. Um nulo é sempre um "não definido"? Com um ponteiro, isso não é claro, pois nulo significa literalmente não alocado ou criado, embora possa , mas pode não necessariamente significar "não definido". Vale ressaltar que um ponteiro deve ser liberado e, em boas práticas, definido como 0, no entanto, como um ponteiro compartilhado, um opcional não requer limpeza explícita, portanto, não há uma preocupação de misturar a limpeza com o opcional não foi definido.

Eu acredito que é sobre clareza de código. A clareza reduz o custo de manutenção e desenvolvimento de código. Uma compreensão clara da intenção do código é incrivelmente valiosa.

O uso de um ponteiro para representar isso exigiria sobrecarregar o conceito de ponteiro. Para representar "nulo" como "não definido", normalmente você pode ver um ou mais comentários através do código para explicar essa intenção. Essa não é uma solução ruim em vez de opcional, no entanto, eu sempre opto pela implementação implícita, em vez de comentários explícitos, pois os comentários não são aplicáveis ​​(como na compilação). Exemplos desses itens implícitos para o desenvolvimento (os artigos em desenvolvimento que são fornecidos apenas para reforçar a intenção) incluem os vários lançamentos no estilo C ++, "const" (especialmente em funções-membro) e o tipo "bool", para citar alguns. Pode-se argumentar que você realmente não precisa desses recursos de código, desde que todos obedeçam a intenções ou comentários.

Kit10
fonte