Quando devo realmente usar noexcept?


509

A noexceptpalavra-chave pode ser aplicada adequadamente a muitas assinaturas de funções, mas não tenho certeza sobre quando devo considerar usá-la na prática. Com base no que li até agora, a adição de última hora noexceptparece abordar algumas questões importantes que surgem quando os construtores de movimento jogam. No entanto, ainda não consigo fornecer respostas satisfatórias para algumas perguntas práticas que me levaram a ler mais sobre isso noexceptem primeiro lugar.

  1. Existem muitos exemplos de funções que eu sei que nunca serão lançadas, mas para as quais o compilador não pode determinar isso sozinho. Devo acrescentar noexceptà declaração da função em todos esses casos?

    Ter que pensar se eu preciso acrescentar ou não noexceptapós cada declaração de função reduziria bastante a produtividade do programador (e, francamente, seria uma chatice). Para quais situações devo ter mais cuidado com o uso noexcepte para quais situações posso me safar do implícito noexcept(false)?

  2. Quando posso realisticamente esperar observar uma melhora no desempenho após o uso noexcept? Em particular, dê um exemplo de código para o qual um compilador C ++ é capaz de gerar um melhor código de máquina após a adição de noexcept.

    Pessoalmente, eu me preocupo com noexcepto aumento da liberdade fornecida ao compilador para aplicar com segurança certos tipos de otimizações. Os compiladores modernos se beneficiam noexceptdessa maneira? Caso contrário, posso esperar que alguns deles o façam no futuro próximo?


40
O código que usa move_if_nothrow(ou whatchamacallit) verá uma melhoria de desempenho se houver um mecanismo de exceção exceto.
R. Martinho Fernandes


5
É move_if_noexcept.
Nikos

Respostas:


180

Eu acho que é muito cedo para dar uma resposta para as "melhores práticas", pois não houve tempo suficiente para usá-lo na prática. Se isso fosse perguntado sobre os especificadores de lançamento logo após serem lançados, as respostas seriam muito diferentes agora.

Ter que pensar se preciso anexar ou não noexceptapós cada declaração de função reduziria bastante a produtividade do programador (e, francamente, seria uma dor).

Bem, use-o quando for óbvio que a função nunca será lançada.

Quando posso realisticamente esperar observar uma melhora no desempenho após o uso noexcept? [...] Pessoalmente, eu me preocupo por noexceptcausa do aumento da liberdade fornecida ao compilador para aplicar com segurança certos tipos de otimizações.

Parece que os maiores ganhos de otimização são de otimizações de usuários, e não de compiladores, devido à possibilidade de verificar noexcepte sobrecarregar. A maioria dos compiladores segue um método de manipulação de exceção sem penalidade, se não jogar, então duvido que isso mude muito (ou qualquer coisa) no nível do código da máquina, embora talvez reduza o tamanho binário removendo o código de manipulação.

Usar noexceptnos quatro grandes (construtores, atribuição, não destruidores, como já são noexcept) provavelmente causará as melhores melhorias, pois as noexceptverificações são 'comuns' no código do modelo, como em stdcontêineres. Por exemplo, std::vectornão usará a movimentação da sua classe a menos que esteja marcada noexcept(ou o compilador pode deduzir isso de outra forma).


6
Acho que o std::terminatetruque ainda obedece ao modelo de custo zero. Ou seja, acontece que o intervalo de instruções nas noexceptfunções é mapeado para chamar std::terminatese throwfor usado em vez do desbobinador da pilha. Portanto, duvido que tenha mais sobrecarga que o rastreamento de exceção regular.
Matthieu M.

4
@Klaim Veja isto: stackoverflow.com/a/10128180/964135 Na verdade, ele só precisa ser não arremessado, mas noexceptgarante isso.
Pubby

3
"Se uma noexceptfunção é lançada, então std::terminateé chamado, o que parece envolver uma pequena quantidade de sobrecarga" ... Não, isso deve ser implementado não gerando tabelas de exceção para essa função, que o expedidor de exceção deve capturar e depois resgatar.
Potatoswatter

8
O tratamento de exceções do @Pubby C ++ geralmente é feito sem sobrecarga, exceto as tabelas de salto que mapeiam o lançamento potencial de endereços de sites de chamada para pontos de entrada do manipulador. A remoção dessas tabelas é o mais próximo possível da remoção completa do tratamento de exceções. A única diferença é o tamanho do arquivo executável. Provavelmente não vale a pena mencionar nada.
Potatoswatter

26
"Bem, use-o quando for óbvio que a função nunca será lançada". Discordo. noexceptfaz parte da interface da função ; você não deve adicioná-lo apenas porque sua implementação atual não é lançada. Eu não estou certo sobre a resposta certa para esta pergunta, mas estou bastante confiante de que a forma como a sua função passa a se comportar hoje não tem nada a ver com isso ...
Nemo

133

Como eu continuo repetindo esses dias: semântica primeiro .

Adicionando noexcept, noexcept(true)e noexcept(false)é antes de tudo sobre semântica. Incidentalmente, condiciona várias otimizações possíveis.

Como programador de leitura de código, a presença de noexcepté semelhante à de const: ela me ajuda a entender melhor o que pode ou não acontecer. Portanto, vale a pena gastar algum tempo pensando se você sabe ou não se a função será lançada. Para um lembrete, qualquer tipo de alocação de memória dinâmica pode ser lançada.


Ok, agora vamos às possíveis otimizações.

As otimizações mais óbvias são realmente realizadas nas bibliotecas. O C ++ 11 fornece uma série de características que permitem saber se uma função é noexceptou não, e a própria implementação da Biblioteca Padrão usará essas características para favorecer noexceptoperações nos objetos definidos pelo usuário que eles manipulam, se possível. Como mover semântica .

O compilador pode raspar apenas um pouco de gordura (talvez) dos dados de manipulação de exceção, porque precisa levar em conta o fato de que você pode ter mentido. Se uma função marcada noexcepté lançada, então std::terminateé chamada.

Essa semântica foi escolhida por dois motivos:

  • beneficiando imediatamente, noexceptmesmo quando as dependências ainda não o usam (compatibilidade com versões anteriores)
  • permitindo a especificação de noexceptquando chamar funções que podem ser lançadas teoricamente, mas não são esperadas para os argumentos fornecidos

2
Talvez eu seja ingênuo, mas imagino que uma função que invoca apenas noexceptfunções não precisaria fazer nada de especial, porque quaisquer exceções que possam surgir são acionadas terminateantes de chegarem a esse nível. Isso difere muito de ter que lidar e propagar uma bad_allocexceção.

8
Sim, é possível definir noexcept da maneira que você sugere, mas esse seria um recurso realmente inutilizável. Muitas funções podem ser ativadas se determinadas condições não forem mantidas e você não pode chamá-las, mesmo que saiba que as condições são atendidas. Por exemplo, qualquer função que possa lançar std :: invalid_argument.
tr3w

3
@MatthieuM. Um pouco atrasado para uma resposta, mas mesmo assim. As funções marcadas como noexcept podem chamar outras funções que podem ser lançadas, a promessa é que essa função não emitirá uma exceção, ou seja, elas apenas precisam lidar com a exceção!
Honf

4
Eu votei esta resposta há muito tempo, mas depois de ler e pensar um pouco mais, tenho um comentário / pergunta. "Mover semântica" é o único exemplo que eu já vi alguém dar onde noexcepté claramente útil / uma boa idéia. Estou começando a pensar em mover construção, mover atribuição e trocar são os únicos casos que existem ... Você conhece algum outro?
Nemo

5
@ Nememo: Na biblioteca Standard, é possivelmente o único, no entanto, exibe um princípio que pode ser reutilizado em outro lugar. Uma operação de movimentação é uma operação que coloca temporariamente algum estado no "limbo" e somente quando é noexceptque alguém pode usá-lo com confiança em um dado que pode ser acessado posteriormente. Eu pude ver essa idéia sendo usada em outro lugar, mas a biblioteca Standard é bastante fina em C ++ e é usada apenas para otimizar cópias de elementos que eu acho.
Matthieu M.

77

Isso realmente faz uma diferença (potencialmente) enorme para o otimizador no compilador. Os compiladores realmente têm esse recurso há anos através da instrução throw () vazia após uma definição de função, bem como extensões de propriedade. Posso garantir que os compiladores modernos aproveitam esse conhecimento para gerar um código melhor.

Quase toda otimização no compilador usa algo chamado "gráfico de fluxo" de uma função para raciocinar sobre o que é legal. Um gráfico de fluxo consiste no que geralmente é chamado de "blocos" da função (áreas de código que possuem uma única entrada e uma única saída) e arestas entre os blocos para indicar para onde o fluxo pode saltar. Noexcept altera o gráfico de fluxo.

Você pediu um exemplo específico. Considere este código:

void foo(int x) {
    try {
        bar();
        x = 5;
        // Other stuff which doesn't modify x, but might throw
    } catch(...) {
        // Don't modify x
    }

    baz(x); // Or other statement using x
}

O gráfico de fluxo para esta função é diferente se barestiver rotulado noexcept(não há como a execução saltar entre o final da barinstrução catch). Quando rotulado como noexcept, o compilador tem certeza de que o valor de x é 5 durante a função baz - diz-se que o bloco x = 5 "domina" o bloco baz (x) sem a borda da bar()instrução catch.

Em seguida, ele pode fazer algo chamado "propagação constante" para gerar código mais eficiente. Aqui, se baz estiver alinhado, as instruções que usam x também podem conter constantes e, em seguida, o que costumava ser uma avaliação em tempo de execução pode ser transformada em uma avaliação em tempo de compilação, etc.

De qualquer forma, a resposta curta: noexceptpermite que o compilador gere um gráfico de fluxo mais rígido, e o gráfico de fluxo é usado para raciocinar sobre todos os tipos de otimizações comuns do compilador. Para um compilador, as anotações de usuários dessa natureza são impressionantes. O compilador tentará descobrir essas coisas, mas geralmente não pode (a função em questão pode estar em outro arquivo de objeto não visível para o compilador ou usar transitivamente alguma função que não é visível) ou, quando o faz, existe alguma exceção trivial que pode ser lançada e que você nem conhece, portanto, não pode rotulá-la implicitamente como noexcept(alocar memória pode gerar bad_alloc, por exemplo).


3
Isso realmente faz diferença na prática? O exemplo é artificial porque nada antes x = 5pode ser lançado. Se essa parte do trybloco tivesse algum objetivo, o raciocínio não seria válido.
Potatoswatter

7
Eu diria que isso faz uma diferença real na otimização de funções que contêm blocos try / catch. O exemplo que dei, embora artificial, não é exaustivo. O ponto mais importante é que noexcept (como a instrução throw () anterior) ajuda a compilar a gerar um gráfico de fluxo menor (menos arestas, menos blocos), que é uma parte fundamental de muitas otimizações que ele faz.
Terry Mahaffey

Como o compilador pode reconhecer que o código pode gerar uma exceção? O acesso à matriz é considerado uma possível exceção?
Tomas Kubes

3
@ qub1n Se o compilador puder ver o corpo da função, poderá procurar por throwinstruções explícitas ou outras coisas assim new. Se o compilador não pode ver o corpo, deve confiar na presença ou ausência de noexcept. O acesso simples à matriz normalmente não gera exceções (o C ++ não possui verificação de limites); portanto, não, o acesso à matriz não faria o compilador pensar que uma função lança exceções. (Acesso Out-of-bound é UB, não uma exceção garantido.)
cdhowie

@cdhowie " deve contar com a presença ou ausência de noexcept " ou presença de throw()em C pré-noexcept ++
curiousguy

57

noexceptpode melhorar drasticamente o desempenho de algumas operações. Isso não acontece no nível de geração de código de máquina pelo compilador, mas selecionando o algoritmo mais eficaz: como outros mencionados, você faz essa seleção utilizando a função std::move_if_noexcept. Por exemplo, o crescimento de std::vector(por exemplo, quando chamamos reserve) deve fornecer uma forte garantia de exceção-segurança. Se ele sabe que To construtor de movimento não joga, ele pode simplesmente mover todos os elementos. Caso contrário, ele deve copiar todos os Ts. Isso foi descrito em detalhes neste post .


4
Adendo: Isso significa que, se você definir construtores de movimentação ou operadores de atribuição de movimentação, adicionar noexcepta eles (se aplicável)! As funções de membro de mudança definidas implicitamente foram noexceptadicionadas automaticamente (se aplicável).
Mucaho 3/11

33

Quando posso realisticamente, exceto observar uma melhora no desempenho após o uso noexcept? Em particular, dê um exemplo de código para o qual um compilador C ++ é capaz de gerar melhor código de máquina após a adição de noexcept.

Hum, nunca? Nunca é um tempo? Nunca.

noexcepté para otimizações de desempenho do compilador da mesma maneira que constpara otimizações de desempenho do compilador. Ou seja, quase nunca.

noexcepté usado principalmente para permitir que "você" detecte em tempo de compilação se uma função pode gerar uma exceção. Lembre-se: a maioria dos compiladores não emite código especial para exceções, a menos que ele realmente exija algo. Então noexceptnão é uma questão de dar as dicas de compilador sobre como otimizar uma função tanto como dando -lhe dicas sobre como usar uma função.

Modelos como move_if_noexceptirão detectar se o construtor de movimentação está definido com noexcepte retornará um em const&vez de um &&do tipo, se não estiver. É uma maneira de dizer para mudar, se é muito seguro fazê-lo.

Em geral, você deve usar noexceptquando achar que será realmente útil fazê-lo. Algum código seguirá caminhos diferentes se is_nothrow_constructiblefor verdadeiro para esse tipo. Se você estiver usando o código que fará isso, sinta-se à vontade para noexceptos construtores apropriados.

Resumindo: use-o para mover construtores e construções semelhantes, mas não sinta que precisa ficar louco com isso.


13
Estritamente, move_if_noexceptnão retornará uma cópia, retornará uma referência de valor constante em vez de uma referência de valor. Em geral, isso fará com que o chamador faça uma cópia em vez de mover, mas move_if_noexceptnão está fazendo a cópia. Caso contrário, ótima explicação.
Jonathan Wakely

12
+1 Jonathan. O redimensionamento de um vetor, por exemplo, moverá os objetos em vez de copiá-los, se o construtor for noexcept. Então esse "nunca" não é verdade.
Mfontanini 28/05

4
Quero dizer, o compilador irá gerar um código melhor nessa situação. O OP está pedindo um exemplo para o qual o compilador é capaz de gerar um aplicativo mais otimizado. Este parece ser o caso (mesmo que não seja uma otimização do compilador ).
Mfontanini 28/05

7
@mfontanini: O compilador gera apenas um código melhor porque é obrigado a compilar um caminho de código diferente . Funciona apenas porque std::vectorfoi escrito para forçar o compilador a compilar código diferente . Não é sobre o compilador detectar algo; é sobre o código do usuário detectar algo.
Nicol Bolas

3
O problema é que não consigo encontrar "otimização do compilador" na citação no início da sua resposta. Como o @ChristianRau disse, o compilador gera um código mais eficiente, não importa qual é a origem dessa otimização. Afinal, o compilador está gerando um código mais eficiente, não é? PS: Eu nunca disse que era uma otimização de compilador, eu até disse "Não é uma otimização de compilador".
Mfontanini 28/05

22

Nas palavras de Bjarne ( The C ++ Programming Language, 4ª edição , página 366):

Onde rescisão for uma resposta aceitável, uma exceção não capturada conseguirá isso porque se transforma em uma chamada de terminação () (§13.5.2.5). Além disso, um noexceptespecificador (§13.5.1.1) pode tornar esse desejo explícito.

Os sistemas tolerantes a falhas são bem-sucedidos em vários níveis. Cada nível lida com o maior número possível de erros sem ficar muito distorcido e deixa o restante em níveis mais altos. Exceções suportam essa visão. Além disso, terminate()suporta essa visão fornecendo um escape se o próprio mecanismo de tratamento de exceções estiver corrompido ou se tiver sido usado de maneira incompleta, deixando assim as exceções não detectadas. Da mesma forma, noexceptfornece uma fuga simples para erros em que tentar recuperar parece inviável.

double compute(double x) noexcept;     {
    string s = "Courtney and Anya";
    vector<double> tmp(10);
    // ...
}

O construtor de vetor pode falhar ao adquirir memória para seus dez duplos e lançar a std::bad_alloc. Nesse caso, o programa termina. Ele termina incondicionalmente invocando std::terminate()(§30.4.1.3). Não invoca destruidores de chamar funções. É definido pela implementação se os destruidores dos escopos entre o throwe o noexcept(por exemplo, para s em compute ()) são chamados. O programa está prestes a terminar, portanto, não devemos depender de nenhum objeto. Ao adicionar um noexceptespecificador, indicamos que nosso código não foi gravado para lidar com um arremesso.


2
Você tem uma fonte para esta cotação?
Anton Golov 11/08/19

5
@AntonGolov "A linguagem de programação C ++, 4ª edição", pág. 366
Rusty Shackleford

1
Parece-me que eu realmente deveria adicionar um noexcepttoda vez, exceto que eu quero explicitamente cuidar de exceções. Sejamos realistas, a maioria das exceções é tão improvável e / ou tão fatal que o resgate é dificilmente razoável ou possível. Por exemplo, no exemplo citado, se a alocação falhar, o aplicativo dificilmente poderá continuar funcionando corretamente.
Neonit 08/08/19

21
  1. Existem muitos exemplos de funções que eu sei que nunca serão lançadas, mas para as quais o compilador não pode determinar isso sozinho. Devo acrescentar noexcept à declaração da função em todos esses casos?

noexcepté complicado, pois faz parte da interface de funções. Especialmente, se você estiver escrevendo uma biblioteca, o código do seu cliente pode depender da noexceptpropriedade. Pode ser difícil alterá-lo mais tarde, pois você pode quebrar o código existente. Isso pode ser menos preocupante quando você está implementando código que é usado apenas pelo seu aplicativo.

Se você tem uma função que não pode ser noexceptexecutada , pergunte-se se ela gostaria de permanecer ou isso restringiria futuras implementações? Por exemplo, você pode querer introduzir uma verificação de erro de argumentos ilegais lançando exceções (por exemplo, para testes de unidade) ou pode depender de outro código de biblioteca que possa alterar sua especificação de exceção. Nesse caso, é mais seguro ser conservador e omitir noexcept.

Por outro lado, se você tem certeza de que a função nunca deve ser lançada e está correto que faz parte da especificação, você deve declará-la noexcept. No entanto, lembre-se de que o compilador não poderá detectar violações noexceptse a sua implementação mudar.

  1. Para quais situações devo ter mais cuidado com o uso de noexcept e para quais situações posso me safar com o implícito noexcept (false)?

Existem quatro classes de funções nas quais você deve se concentrar porque elas provavelmente terão o maior impacto:

  1. mover operações (mover operador de atribuição e mover construtores)
  2. operações de swap
  3. desalocadores de memória (exclusão do operador, exclusão do operador [])
  4. destruidores (embora estes sejam implicitamente, a noexcept(true)menos que você os faça noexcept(false))

Essas funções geralmente devem ser noexcepte é mais provável que as implementações da biblioteca possam fazer uso da noexceptpropriedade. Por exemplo, std::vectorpode usar operações de movimentação sem arremesso sem sacrificar fortes garantias de exceção. Caso contrário, ele terá que voltar aos elementos de cópia (como no C ++ 98).

Esse tipo de otimização está no nível algorítmico e não depende de otimizações do compilador. Pode ter um impacto significativo, especialmente se os elementos forem caros de copiar.

  1. Quando posso realisticamente esperar observar uma melhora no desempenho depois de usar o noexcept? Em particular, dê um exemplo de código para o qual um compilador C ++ é capaz de gerar melhor código de máquina após a adição de noexcept.

A vantagem da noexceptespecificação contra nenhuma exceção ou throw()é que o padrão permite aos compiladores mais liberdade quando se trata de empilhar o desenrolamento. Mesmo no throw()caso, o compilador precisa desenrolar completamente a pilha (e deve fazê-lo na ordem inversa exata das construções do objeto).

No noexceptcaso, por outro lado, não é necessário fazer isso. Não é necessário que a pilha precise ser desenrolada (mas o compilador ainda pode fazê-lo). Essa liberdade permite uma otimização adicional do código, pois diminui a sobrecarga de ser sempre capaz de relaxar a pilha.

A pergunta relacionada sobre noexcept, desenrolamento de pilha e desempenho entra em mais detalhes sobre a sobrecarga quando é necessário desenrolar a pilha.

Eu também recomendo o livro de Scott Meyers "Effective Modern C ++", "Item 14: Declarar funções como" Exceto se não emitirem exceções "para leitura posterior.


Ainda assim, faria muito mais sentido se as exceções fossem implementadas em C ++ como em Java, onde você marca o método que pode ser lançado com a throwspalavra - chave em vez de noexceptnegativa. Eu simplesmente não pode obter alguns dos C ++ escolhas de design ...
doc

Eles o nomearam noexceptporque throwjá foram tirados. Simplificando, throwpode ser usado quase do jeito que você menciona, exceto que eles estragaram o design, tornando-o quase inútil - até prejudicial. Mas estamos presos a ela agora, já que removê-la seria uma mudança radical com pouco benefício. Então noexcepté basicamente throw_v2.
AnorZaken

Como thrownão é útil?
Curiousguy

@curiousguy "throw" em si (para lançar exceções) é útil, mas "throw" como um especificador de exceção foi descontinuado e, no C ++ 17, foi removido. Por razões pelas quais os especificadores de exceção não são úteis, consulte esta pergunta: stackoverflow.com/questions/88573/…
Philipp Claßen

1
@ PhilippClaßen O throw()especificador de exceção não forneceu a mesma garantia que nothrow?
Curiousguy

17

Existem muitos exemplos de funções que eu sei que nunca serão lançadas, mas para as quais o compilador não pode determinar isso sozinho. Devo acrescentar noexcept à declaração da função em todos esses casos?

Quando você diz "Eu sei que eles nunca serão lançados", você quer dizer que, examinando a implementação da função, você sabe que a função não será lançada. Eu acho que essa abordagem é de dentro para fora.

É melhor considerar se uma função pode lançar exceções para fazer parte do design da função: tão importante quanto a lista de argumentos e se um método é um mutador (... const). Declarar que "essa função nunca gera exceções" é uma restrição na implementação. Omitir isso não significa que a função possa gerar exceções; isso significa que a versão atual da função e todas as versões futuras podem gerar exceções. É uma restrição que dificulta a implementação. Mas alguns métodos devem ter a restrição de serem praticamente úteis; mais importante, para que possam ser chamados de destruidores, mas também para a implementação de código de "reversão" em métodos que fornecem a garantia de exceção forte.


Esta é a melhor resposta de longe. Você está fazendo uma garantia aos usuários do seu método, que é outra maneira de dizer que você está restringindo sua implementação para sempre (sem quebras). Obrigado pela perspectiva esclarecedora.
AnorZaken

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.