Qual é a razão para não usar o [[nodiscard]] do C ++ 17 em quase todos os lugares em um novo código?

57

O C ++ 17 introduz o atributo [[nodiscard]] , que permite que os programadores marquem as funções de forma que o compilador produza um aviso se o objeto retornado for descartado por um chamador; o mesmo atributo pode ser adicionado a um tipo de classe inteiro.

Li sobre a motivação para esse recurso no original proposta , e sei que o C ++ 20 adicionará o atributo a funções padrão, como std::vector::empty , cujos nomes não transmitem um significado não ambíguo em relação ao valor de retorno.

É um recurso legal e útil. De fato, quase parece muito útil. Em todo lugar que eu leio sobre [[nodiscard]] , as pessoas discutem isso como se você tivesse acabado de adicioná-lo a algumas poucas funções ou tipos e esquecer o resto. Mas por que um valor não descartável deveria ser um caso especial, especialmente ao escrever um novo código? Não é um valor de retorno descartado tipicamente um bug ou pelo menos um desperdício de recursos?

E não é um dos princípios de design do próprio C ++ que o compilador deve capturar tantos erros quanto possível?

Em caso afirmativo, por que não adicionar [[nodiscard]] em seu próprio código não herdado a quase todas as funções não- void e quase todos os tipos de classe?

Eu tentei fazer isso no meu próprio código, e funciona bem, exceto que é tão terrivelmente detalhado que ele começa a parecer Java. Parece muito mais natural fazer compiladores alertar sobre valores de retorno descartados por padrão exceto para os poucos outros casos em que você marca sua intenção [*] .

Como eu já vi zero discussões sobre essa possibilidade em propostas padrão, entradas de blog, perguntas do Stack Overflow ou em qualquer outro lugar na internet, devo estar perdendo alguma coisa.

Por que essa mecânica não faz sentido no novo código C ++? A verbosidade é a única razão para não usar [[nodiscard]] em quase todos os lugares?

[*] Em teoria, você pode ter algo como um atributo [[maydiscard]] , que também pode ser adicionado retroativamente a funções como printf em implementações de biblioteca padrão.

    
por Christian Hackl 30.12.2017 / 18:08
fonte

3 respostas

60

Em um código novo que não precisa ser compatível com padrões mais antigos, use esse atributo onde quer que seja sensato. Mas para C ++, [[nodiscard]] faz um padrão incorreto. Você sugere:

It would seem much more natural to make compilers warn about discarded return values by default except for the few other cases where you mark your intention.

Isso faria com que o código correto existente emitisse muitos avisos. Embora essa mudança tecnicamente possa ser considerada compatível com versões anteriores, já que qualquer código existente ainda é compilado com sucesso, isso seria uma grande mudança de semântica na prática.

Decisões de design para uma linguagem madura existente com uma base de código muito grande são necessariamente diferentes das decisões de design para um idioma completamente novo. Se esta fosse uma nova língua, então advertir por padrão seria sensato. Por exemplo, a linguagem Nim requer que valores desnecessários sejam descartados explicitamente - isso seria semelhante a envolver todas as declarações de expressão em C ++ com um cast (void)(...) .

Um atributo [[nodiscard]] é mais útil em dois casos:

  • se uma função não tiver efeitos além de retornar um determinado resultado, ou seja, é pura. Se o resultado não for usado, a chamada é certamente inútil. Por outro lado, descartar o resultado não seria incorreto.

  • se o valor de retorno precisar ser verificado, por exemplo para uma interface semelhante a C que retorna códigos de erro em vez de lançar. Este é o caso de uso principal. Para o idiomático C ++, isso será bastante raro.

Estes dois casos deixam um enorme espaço de funções impuras que retornam um valor, onde tal aviso seria enganoso. Por exemplo:

  • Considere um tipo de dados da fila com um método .pop() que remove um elemento e retorna uma cópia do elemento removido. Tal método é frequentemente conveniente. No entanto, existem alguns casos em que apenas queremos remover o elemento, sem obter uma cópia. Isso é perfeitamente legítimo e um aviso não seria útil. Um design diferente (como std::vector ) divide essas responsabilidades, mas isso tem outras compensações.

    Note que, em alguns casos, uma cópia deve ser feita de qualquer maneira, então, graças ao retorno da RVO, a cópia será gratuita.

  • Considere interfaces fluentes, onde cada operação retorna o objeto para que outras operações possam ser realizadas. Em C ++, o exemplo mais comum é o operador de inserção de fluxo << . Seria extremamente complicado adicionar um atributo [[nodiscard]] a cada << de sobrecarga.

Estes exemplos demonstram que fazer código idiomático em C ++ compilar sem avisos sob uma linguagem “C ++ 17 com nodiscard por padrão” seria bastante entediante.

Observe que o seu novo e brilhante código C ++ 17 (onde você pode usar esses atributos) ainda pode ser compilado junto com bibliotecas que visam padrões C ++ mais antigos. Essa compatibilidade com versões anteriores é crucial para o ecossistema C / C ++. Assim, tornar o padrão nodiscard resultaria em muitos avisos para casos de uso idiomáticos típicos - avisos que você não pode corrigir sem alterações de longo alcance no código-fonte da biblioteca.

Indiscutivelmente, o problema aqui não é a mudança de semântica, mas que os recursos de cada padrão C ++ se aplicam a um escopo de unidade de compilação e não a um escopo por arquivo. Se / quando algum padrão futuro de C ++ se afastar dos arquivos de cabeçalho, tal mudança seria mais realista.

    
por 30.12.2017 / 18:36
fonte
10

[[nodiscard]] é apenas uma dica

Como Andrew Tomazos explicou em P0189R0 , [[nodiscard]] é uma dica do designer, não o requisito. Um usuário pode querer descartar intencionalmente os resultados da função e o designer não deve negar suas invenções.

The existing practice demonstrates there are cases when the programmer intentionally wants to discard the result of a nodiscard function, even though in most cases they do not. The existing nodiscard is a hint from the function designer to the function user, that immediately destroying the result is most likely not what you want, but it isn’t a straight­jacket and isn’t used as such.

Após essa declaração, os dois casos em que [[nodiscard]] attribute poderia causar um erro ou nodiscard keyword poderia causar um comportamento indefinido, foram lançados em favor do atributo [[nodiscard]] , causando apenas um aviso.

% de uso [[nodiscard]] adicional

Além da resposta de @ amon, como Herb Sutter apontou em Relatório de viagem: Reunião de padrões ISO C ++ de inverno , o atributo [[nodiscard]] tem dois usos adicionais:

  • A anotação de um tipo a aplica-se a todos os usos desse tipo como um valor retornado, como no caso de std::async , onde ignorar o valor de retorno significaria não ser assíncrono.
  • Indicando uma diferença para evitar erro , como no caso de usar empty() e clear() , assim, por exemplo, o usuário saberá que chamar empty() não terá efeitos colaterais se chamado por engano.

Canonical examples include the return value of std::async where ignoring the return value would mean being not async at all, and the return value of a container’s empty() function where people sometimes confuse empty() and clear() and we don’t want them to think calling empty() has side effects if they call it by mistake.

[[nodiscard]] significado implícito para recursos que requerem funções

  • Observe que a seção a seguir está relacionada apenas ao compilador do Visual Studio com /analyze ou, de outra forma, ativado C6031 warning.

A documentação do Visual Studio em C6031 afirma que [[nodiscard]] está implícito para recursos que requerem funções, como disco, rede ou memória.

In general, it is not safe to assume that a call to function requiring disk, network, memory, or other resources will always succeed.

Acontece que o significado [[nodiscard]] está implícito para tais funções, e o chamador deve sempre prestar atenção ao valor do resultado. Descartar o valor de tal função também emite um C6031 aviso se ativado ( ou seja compilado com /analyze ).

Exemplo

No exemplo a seguir, o descarte std::fopen resultará em C6031 de aviso, mesmo Embora std::fopen não tenha um atributo [[nodiscard]] explícito.

           fopen("/._1", "r"); // C6031
FILE* Fp = fopen("/._2", "r"); // OK

The caller should always check the return value and handle error cases appropriately.

estendido [[nodiscard]] : deve inspecionar resultado

O Visual Studio vai ainda mais longe, permitindo a função de marcação com _Must_inspect_result_ que não apenas proíbe o descarte de valor, mas obriga o usuário a verificar seu valor em vez de lançá-lo para void .

Also consider using the _Must_inspect_result_ annotation, which checks that the value is examined in a useful way.

O

_Must_inspect_result_ valor de retorno anotado deve ser inspecionado ou aviso será levantado.

"Inspection" includes whether it is used in a conditional expression, is assigned to an output parameter or global, or is passed as a parameter.

Exemplo

No exemplo a seguir, o resultado do descarte causará o aviso C6031 , mas atribuí-lo sem inspecionar causará um aviso C28193 . Somente quando finalmente inspecionarmos o resultado, nenhum aviso será emitido.

_Must_inspect_result_ bool f__Must_inspect_result_(){ return true; };

           f__Must_inspect_result_();         // C6031
bool  b2 = f__Must_inspect_result_();         // C28193
bool  b3 = f__Must_inspect_result_(); if(b3); // OK

In some rare instances, the return value is intentionally not used. The most common of these instances is where a string length is returned but not actually used before some other test is made.

Meus pensamentos: [[maybe_noinspect]]

Como é possível diferenciar dois tipos de [[nodiscard]] :

  • padrão significado: aviso de problema no resultado descartado
  • extended significado: aviso de problema no resultado não inspecionado

Vou fazer uma pergunta retórica: por que não usar os atributos [[nodiscard]] e [[maybe_noinspect]] ?

  • [[nodiscard]] attribute pode permanecer inalterado, o que significa que o resultado não deve ser descartado, mas crie um aviso se o valor retornado não for inspecionado, até que [[maybe_noinspect]] atributo esteja presente.
  • [[maybe_noinspect]] attribute pode significar que o valor retornado pode não ser inspecionado, ignorando o aviso causado por um valor não-discardado, mas não-inspecionado, retornado com o atributo [[nodiscard]] .

[[maybe_noinspect]] pode ser usado junto com [[nodiscard]] ou sozinho, possivelmente envolvendo o atributo [[nodiscard]] no segundo caso.

Exemplo

Se tivéssemos [[maybe_noinspect]] e implicaria [[nodiscard]] , poderíamos estender [[nodiscard]] , com a possibilidade de transformar o significado padrão de volta em [[maybe_noinspect]] .

[[nodiscard]]       bool f_nodiscard()      { return true; };
[[maybe_noinspect]] bool f_maybe_noinspect(){ return true; };

          f_nodiscard();               // Warning, value is discarded
bool b1 = f_nodiscard();               // Warning, value is uninspected
bool b2 = f_nodiscard(); if(b2);       // OK,      value is inspected

          f_maybe_noinspect();         // Warning, value is discarded
bool b3 = f_maybe_noinspect();         // OK,      value may be not inspected
bool b4 = f_maybe_noinspect(); if(b4); // OK,      value is inspected
    
por 31.12.2017 / 12:41
fonte
6

Razões que eu não iria [[nodiscard]] em quase todos os lugares:

  1. (major :) Isso introduziria way muito barulho em meus cabeçalhos.
  2. (major :) Não sinto que deva fazer strongs suposições especulativas sobre o código de outras pessoas. Você quer descartar o valor de retorno que eu te dou? Tudo bem, se mate.
  3. (menor :) Você estaria garantindo incompatibilidade com o C ++ 14

Agora, se você fez o padrão, você estaria forçando todos os desenvolvedores de bibliotecas a forçar todos os usuários a não descartar os valores de retorno. Isso seria horrível. Ou você os força a adicionar [[maydiscard]] em inúmeras funções, o que também é horrível.

    
por 31.12.2017 / 02:39
fonte

Tags