Qual é o motivo para não usar o C ++ 17 [[nodiscard]] em quase todo lugar no novo código?

70

O C ++ 17 apresenta o [[nodiscard]]atributo, que permite aos programadores marcar funções de uma maneira 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.

Eu li sobre a motivação para esse recurso na proposta original 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 inequívoco em relação ao valor de retorno.

É um recurso interessante e útil. De fato, quase parece muito útil. Em todos os lugares em que leio [[nodiscard]], as pessoas discutem isso como se você apenas o adicionasse a algumas poucas funções ou tipos e esquece o resto. Mas por que um valor não descartável deve ser um caso especial, especialmente ao escrever um novo código? Um valor de retorno descartado não é 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 o maior número possível de erros?

Se sim, por que não adicionar [[nodiscard]]seu próprio código não legado a quase todas as não voidfunções e quase todos os tipos de classe?

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

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

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


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

Christian Hackl
fonte
22
Bem, há várias funções em que o valor de retorno pode ser descartado com sensibilidade. operator =por exemplo. E std::map::insert.
user253751
2
@immibis: Verdade. A biblioteca padrão está cheia dessas funções. Mas no meu próprio código, eles tendem a ser extremamente raros; você acha que é uma questão de código de biblioteca versus código de aplicativo?
Christian Hackl
9
"... terrivelmente detalhado que começa a parecer Java ..." - ler isso em uma pergunta sobre C ++ me fez tremer um pouco: - /
Marco13
3
@ChristianHackl Os comentários provavelmente não são o lugar certo para "bashing de linguagem" e, é claro, Java também é detalhado se comparado a outros idiomas. Mas os arquivos de cabeçalho, os operadores de atribuição, os construtores de cópias e o uso adequado de constpodem inchar uma classe "simples", caso contrário, (ou melhor, "objeto de dados antigo simples") consideravelmente em C ++.
precisa saber é o seguinte
2
@ Marco13: Não consigo abordar tantos pontos em um comentário. Vamos resumir: Java não possui arquivos de cabeçalho, o que reduz a contagem de arquivos, mas aumenta o acoplamento; OTOH obriga você a colocar todas as classes de nível superior em seu próprio arquivo e todas as funções em uma classe. No C ++, você quase nunca implementa operadores de atribuição e copia os próprios construtores; essas funções são mais relevantes para classes de biblioteca padrão como std::vectorou std::unique_ptr, das quais você só precisa de membros de dados em sua definição de classe. Eu trabalhei com os dois idiomas; Java é uma linguagem aceitável, mas é mais detalhada.
Christian Hackl

Respostas:

73

No novo código que não precisa ser compatível com os padrões mais antigos, use esse atributo sempre que for sensato. Mas para C ++, [[nodiscard]]faz um padrão ruim. Você sugere:

Parece muito mais natural fazer com que os compiladores avisem sobre os valores de retorno descartados por padrão, exceto nos poucos outros casos em que você marca sua intenção.

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

As decisões de design para uma linguagem existente e madura, com uma base de código muito grande, são necessariamente diferentes das decisões de design para uma linguagem completamente nova. Se esse fosse um novo idioma, o aviso por padrão seria sensato. Por exemplo, a linguagem Nim exige que valores desnecessários sejam descartados explicitamente - isso seria semelhante a agrupar todas as instruções de expressão em C ++ com uma conversão (void)(...).

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

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

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

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

  • Considere um tipo de dados da fila com um .pop()método que remove um elemento e retorna uma cópia do elemento removido. Esse método geralmente é 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 possui outras vantagens e desvantagens.

    Observe que, em alguns casos, é necessário fazer uma cópia de qualquer maneira, portanto, graças ao RVO, a devolução da cópia seria gratuita.

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

Esses exemplos demonstram que tornar o código C ++ idiomático compilado sem avisos sob a linguagem "C ++ 17 com nodiscard por padrão" seria bastante tedioso.

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

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

amon
fonte
6
Eu votei isso de positivo, pois ele contém informações úteis. Ainda não tenho certeza se realmente responde à pergunta. Funções impuras como popor operator<<, aquelas seriam explicitamente marcadas pelo meu [[maydiscard]]atributo imaginário , de modo que nenhum aviso seria gerado. Você está dizendo que essas funções geralmente são muito mais comuns do que eu gostaria de acreditar, ou muito mais comuns em geral do que nas minhas próprias bases de código? Seu primeiro parágrafo sobre o código existente é certamente verdadeiro, mas ainda me faz pensar se alguém mais está aplicando todo o seu novo código e [[nodiscard]], se não, por que não.
Christian Hackl
23
@ChristianHackl: Houve um tempo na história do C ++ em que era considerado uma boa prática retornar valores como const. Você não pode dizer returns_an_int() = 0, então por que deveria returns_a_str() = ""funcionar? O C ++ 11 mostrou que essa era realmente uma péssima idéia, porque agora todo esse código estava proibindo movimentos porque os valores eram const. Eu acho que a lição apropriada dessa lição é "não depende de você o que seus chamadores fazem com o resultado". Ignorar o resultado é uma coisa perfeitamente válida que os chamadores podem querer fazer e, a menos que você saiba que está errado (uma barra muito alta), você não deve bloqueá-lo.
GManNickG
13
Um cartão de visita empty()também é sensível porque o nome é bastante ambíguo: é um predicado is_empty()ou é um mutador clear()? Você normalmente não espera que esse mutador retorne nada, portanto um aviso sobre um valor de retorno pode alertar o programador sobre um problema. Com um nome mais claro, esse atributo não seria tão necessário.
amon
13
@ChristianHackl: Como outros já disseram, acho que funções puras são um bom exemplo de quando isso deve ser usado. Eu daria um passo adiante e diria que deveria haver um [[pure]]atributo, e isso deveria implicar [[nodiscard]]. Dessa forma, fica claro por que esse resultado não deve ser descartado. Mas, além de funções puras, não consigo pensar em outra classe de funções que devam ter esse comportamento. E a maioria das funções não é pura, portanto, não acho que o nodiscard como padrão é a escolha certa.
GManNickG
5
@GManNickG: Tendo tentado e falhado ao depurar o código que ignorava os códigos de erro, acredito firmemente que a grande maioria dos códigos de erro precisa ser verificada por padrão.
Mooing Duck
9

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

  1. (major :) Este introduziria maneira muito barulho em meus cabeçalhos.
  2. (major :) Eu não acho que devo fazer suposições especulativas fortes sobre o código de outras pessoas. Você quer descartar o valor de retorno que eu te dou? Tudo bem, nocauteie-se.
  3. (menor :) Você estaria garantindo incompatibilidade com o C ++ 14

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

einpoklum - restabelece Monica
fonte
0

Como exemplo: operator << possui um valor de retorno que depende da chamada absolutamente necessária ou absolutamente inútil. (std :: cout << x << y, o primeiro é necessário porque retorna o fluxo, o segundo não serve para nada). Agora compare com printf, onde todos descartam o valor de retorno que existe para verificação de erros, mas o operador << não tem verificação de erros, portanto, não pode ter sido útil em primeiro lugar. Portanto, nos dois casos, o nodiscard seria apenas prejudicial.

gnasher729
fonte
Isso responde a uma pergunta que não foi feita, ou seja, "Qual o motivo de não usar em [[nodiscard]]todos os lugares?".
Christian Hackl