A noexcept
palavra-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 noexcept
parece 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 noexcept
em primeiro lugar.
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
noexcept
apó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 usonoexcept
e para quais situações posso me safar do implícitonoexcept(false)
?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 denoexcept
.Pessoalmente, eu me preocupo com
noexcept
o aumento da liberdade fornecida ao compilador para aplicar com segurança certos tipos de otimizações. Os compiladores modernos se beneficiamnoexcept
dessa maneira? Caso contrário, posso esperar que alguns deles o façam no futuro próximo?
move_if_nothrow
(ou whatchamacallit) verá uma melhoria de desempenho se houver um mecanismo de exceção exceto.move_if_noexcept
.Respostas:
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.
Bem, use-o quando for óbvio que a função nunca será lançada.
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
noexcept
e 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
noexcept
nos quatro grandes (construtores, atribuição, não destruidores, como já sãonoexcept
) provavelmente causará as melhores melhorias, pois asnoexcept
verificações são 'comuns' no código do modelo, como emstd
contêineres. Por exemplo,std::vector
não usará a movimentação da sua classe a menos que esteja marcadanoexcept
(ou o compilador pode deduzir isso de outra forma).fonte
std::terminate
truque ainda obedece ao modelo de custo zero. Ou seja, acontece que o intervalo de instruções nasnoexcept
funções é mapeado para chamarstd::terminate
sethrow
for usado em vez do desbobinador da pilha. Portanto, duvido que tenha mais sobrecarga que o rastreamento de exceção regular.noexcept
garante isso.noexcept
função é lançada, entãostd::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.noexcept
faz 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 ...Como eu continuo repetindo esses dias: semântica primeiro .
Adicionando
noexcept
,noexcept(true)
enoexcept(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 à deconst
: 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 é
noexcept
ou não, e a própria implementação da Biblioteca Padrão usará essas características para favorecernoexcept
operaçõ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ãostd::terminate
é chamada.Essa semântica foi escolhida por dois motivos:
noexcept
mesmo quando as dependências ainda não o usam (compatibilidade com versões anteriores)noexcept
quando chamar funções que podem ser lançadas teoricamente, mas não são esperadas para os argumentos fornecidosfonte
noexcept
funções não precisaria fazer nada de especial, porque quaisquer exceções que possam surgir são acionadasterminate
antes de chegarem a esse nível. Isso difere muito de ter que lidar e propagar umabad_alloc
exceção.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?noexcept
que 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.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:
O gráfico de fluxo para esta função é diferente se
bar
estiver rotuladonoexcept
(não há como a execução saltar entre o final dabar
instrução catch). Quando rotulado comonoexcept
, 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 dabar()
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:
noexcept
permite 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 comonoexcept
(alocar memória pode gerar bad_alloc, por exemplo).fonte
x = 5
pode ser lançado. Se essa parte dotry
bloco tivesse algum objetivo, o raciocínio não seria válido.throw
instruções explícitas ou outras coisas assimnew
. Se o compilador não pode ver o corpo, deve confiar na presença ou ausência denoexcept
. 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.)throw()
em C pré-noexcept ++noexcept
pode 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çãostd::move_if_noexcept
. Por exemplo, o crescimento destd::vector
(por exemplo, quando chamamosreserve
) deve fornecer uma forte garantia de exceção-segurança. Se ele sabe queT
o construtor de movimento não joga, ele pode simplesmente mover todos os elementos. Caso contrário, ele deve copiar todos osT
s. Isso foi descrito em detalhes neste post .fonte
noexcept
a eles (se aplicável)! As funções de membro de mudança definidas implicitamente foramnoexcept
adicionadas automaticamente (se aplicável).Hum, nunca? Nunca é um tempo? Nunca.
noexcept
é para otimizações de desempenho do compilador da mesma maneira queconst
para 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ãonoexcept
nã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_noexcept
irão detectar se o construtor de movimentação está definido comnoexcept
e retornará um emconst&
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
noexcept
quando achar que será realmente útil fazê-lo. Algum código seguirá caminhos diferentes seis_nothrow_constructible
for verdadeiro para esse tipo. Se você estiver usando o código que fará isso, sinta-se à vontade paranoexcept
os construtores apropriados.Resumindo: use-o para mover construtores e construções semelhantes, mas não sinta que precisa ficar louco com isso.
fonte
move_if_noexcept
nã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, masmove_if_noexcept
não está fazendo a cópia. Caso contrário, ótima explicação.noexcept
. Então esse "nunca" não é verdade.std::vector
foi 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.Nas palavras de Bjarne ( The C ++ Programming Language, 4ª edição , página 366):
fonte
noexcept
toda 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.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 danoexcept
propriedade. 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
noexcept
executada , 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 omitirnoexcept
.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çõesnoexcept
se a sua implementação mudar.Existem quatro classes de funções nas quais você deve se concentrar porque elas provavelmente terão o maior impacto:
noexcept(true)
menos que você os façanoexcept(false)
)Essas funções geralmente devem ser
noexcept
e é mais provável que as implementações da biblioteca possam fazer uso danoexcept
propriedade. Por exemplo,std::vector
pode 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.
A vantagem da
noexcept
especificação contra nenhuma exceção outhrow()
é que o padrão permite aos compiladores mais liberdade quando se trata de empilhar o desenrolamento. Mesmo nothrow()
caso, o compilador precisa desenrolar completamente a pilha (e deve fazê-lo na ordem inversa exata das construções do objeto).No
noexcept
caso, 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.
fonte
throws
palavra - chave em vez denoexcept
negativa. Eu simplesmente não pode obter alguns dos C ++ escolhas de design ...noexcept
porquethrow
já foram tirados. Simplificando,throw
pode 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ãonoexcept
é basicamentethrow_v2
.throw
não é útil?throw()
especificador de exceção não forneceu a mesma garantia quenothrow
?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.fonte