Ao desenvolver em C ++, você deve escrever código periodicamente, no qual exceções não devem ocorrer. Por exemplo, quando precisamos escrever uma troca sem exceção para tipos nativos ou definir uma instrução move noexcept para nossa classe, ou implementar manualmente um destruidor não trivial.
No C ++ 11, o modificador noexcept foi adicionado à linguagem, o que permite ao desenvolvedor entender que as exceções da função (ou método) marcadas com noexcept não podem ser descartadas. Portanto, funções com essa marca podem ser usadas com segurança em contextos onde as exceções não devem surgir.
Por exemplo, se eu tiver esses tipos e funções:
class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r);
e há uma certa classe resources_owner
que possui objetos como first_resource
e second_resource
:
class resources_owner { first_resource first_resource_; second_resource second_resource_; ... };
então eu posso escrever o destruidor resources_owner
seguinte maneira:
resources_owner::~resources_owner() noexcept {
De certa forma, no exceto em C ++ 11, a vida de um desenvolvedor de C ++ foi mais fácil. Mas a implementação atual, exceto no C ++ moderno, tem um lado desagradável ...
O compilador não ajuda a controlar o conteúdo de funções e métodos noexcept
Suponha que no exemplo acima eu tenha me enganado: por alguma razão, considerei release()
marcado como noexcept, mas, na realidade, não é e pode gerar exceções. Isso significa que, quando escrevo um destruidor usando uma release()
:
resources_owner::~resources_owner() noexcept { release(first_resource_);
então eu imploro por problemas. Mais cedo ou mais tarde, este release()
lançará uma exceção e todo o meu aplicativo falhará devido ao chamado automaticamente std::terminate()
. Será ainda pior se meu aplicativo não travar, mas o de outra pessoa, na qual eles usaram minha biblioteca com um destruidor tão problemático para o resources_owner
.
Ou outra variação do mesmo problema. Suponha que não me enganei que release()
fato marcado como sem exceção. Foi sim
Foi marcado na versão 1.0 de uma biblioteca de terceiros da qual tirei first_resource
e release()
. E, depois de vários anos, atualizei para a versão 3.0 desta biblioteca, mas na versão 3.0, release()
não possui mais um modificador noexcept.
Bem o que? A nova versão principal, eles poderiam facilmente quebrar a API.
Somente agora, provavelmente, vou me esquecer de corrigir a implementação do destruidor resources_owner
. E se, em vez de mim, alguém estiver envolvido no suporte ao resource_owner
, que nunca examinou esse destruidor, as alterações na assinatura do release()
provavelmente passarão despercebidas.
Portanto, eu pessoalmente não gosto do fato de o compilador não avisar o programador de forma alguma que o programador dentro do método / função noexcept faça uma chamada de método / função que lança exceções.
Seria melhor se o compilador emitisse esses avisos.
O resgate do afogamento é obra dos próprios afogamentos
OK, o compilador não dá nenhum aviso. E nada pode ser feito sobre esse desenvolvedor simples. Não lide com modificações do compilador C ++ para suas próprias necessidades. Especialmente se você precisar usar não um compilador, mas versões diferentes de diferentes compiladores C ++.
É possível obter ajuda do compilador sem entrar em suas miudezas? I.e. É possível fazer algum tipo de ferramenta para controlar o conteúdo de métodos / funções sem exceção, mesmo que o método dendro-fecal?
Você pode. Desleixado, mas possível.
De onde as pernas crescem?
A abordagem descrita neste artigo foi testada na prática ao preparar a próxima versão do nosso pequeno servidor HTTP incorporado RESTinio .
O fato é que, como o RESTinio está cheio de funcionalidades, perdemos de vista os problemas de segurança de exceção em vários locais. Em particular, com o tempo, ficou claro que, às vezes, as exceções podem surgir dos retornos de chamada enviados ao Asio (o que não deveria ser), bem como as exceções, em princípio, podem ocorrer durante a limpeza de recursos.
Felizmente, na prática, esses problemas nunca foram manifestados, mas a dívida técnica se acumulou e algo teve que ser feito a respeito. E você tinha que fazer algo com o código que já estava escrito. I.e. código não-exceção de trabalho deve ser convertido em código não-exceção de trabalho.
Isso foi feito com a ajuda de várias macros, organizadas por código nos lugares certos. Por exemplo, um caso trivial:
template< typename Message_Builder > void trigger_error_and_close( Message_Builder msg_builder ) noexcept {
E aqui está um fragmento menos trivial:
void reset() noexcept { RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.empty()); RESTINIO_STATIC_ASSERT_NOEXCEPT( m_context_table.pop_response_context_nonchecked()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front().dequeue_group()); RESTINIO_STATIC_ASSERT_NOEXCEPT(make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed)); for(; !m_context_table.empty(); m_context_table.pop_response_context_nonchecked() ) { const auto ec = make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed ); auto & current_ctx = m_context_table.front(); while( !current_ctx.empty() ) { auto wg = current_ctx.dequeue_group(); restinio::utils::suppress_exceptions_quietly( [&] { wg.invoke_after_write_notificator_if_exists( ec ); } ); } } }
O uso dessas macros apertou as mãos várias vezes, apontando para lugares que eu inadvertidamente havia percebido como não-aceitáveis, mas que não eram.
Portanto, a abordagem descrita abaixo, é claro, é um auto-fabricado com rodas quadradas, mas continua ... quero dizer que funciona.
Ainda neste artigo, discutiremos a implementação que foi isolada do código RESTinio em um conjunto separado de macros.
A essência da abordagem
A essência da abordagem é passar a instrução / operador (stmt), que precisa ser verificada quanto à exceção, para uma determinada macro. Essa macro usa static_assert(noexcept(stmt), msg)
para verificar se stmt é realmente noexcept e, em seguida, substitui stmt no código.
Essencialmente, isto é:
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
será substituído por algo como:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
Por que a escolha foi feita em favor das macros?
Em princípio, era possível static_assert(noexcept(...))
sem macros e escrever static_assert(noexcept(...))
no código imediatamente antes das ações serem verificadas. Mas as macros têm pelo menos algumas virtudes que inclinam a balança a favor do uso específico de macros.
Primeiro, as macros reduzem a duplicação de código. Há uma comparação:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
e
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
é claro que, com macros, a expressão principal, ou seja, release(some_resource)
pode ser gravado apenas uma vez. Isso reduz a probabilidade de o código "rastejar" ao longo do tempo, com seu acompanhamento, quando uma correção foi feita em um local e esquecida no segundo.
Em segundo lugar, as macros e, consequentemente, as verificações ocultas por trás delas podem ser facilmente desabilitadas. Digamos, se a abundância de static_assert-s começar a afetar adversamente a velocidade de compilação (embora eu não tenha notado esse efeito). Ou, mais importante, ao atualizar algumas bibliotecas de terceiros, os erros de compilação de static_assert ocultos atrás das macros podem polvilhar diretamente com o rio. Desativar temporariamente as macros pode permitir uma atualização suave do código, incluindo as macros de verificação sequencialmente primeiro em um arquivo, depois no segundo, depois no terceiro, etc.
Portanto, as macros, embora sejam um recurso desatualizado e altamente controverso em C ++, nesse caso em particular, a vida do desenvolvedor é simplificada.
Macro principal ENSURE_NOEXCEPT_STATEMENT
A macro principal ENSURE_NOEXCEPT_STATEMENT é implementada trivialmente:
#define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false)
É usado para verificar se os métodos / funções chamados são de fato exceto e que suas chamadas não precisam ser enquadradas por blocos try-catch. Por exemplo:
class some_complex_container { one_container first_data_part_; another_container second_data_part_; ... public: friend void swap(some_complex_container & a, some_complex_container & b) noexcept { using std::swap;
Além disso, também há a macro ENSURE_NOT_NOEXCEPT_STATEMENT. É usado para garantir que um bloco try-catch adicional seja necessário em torno da chamada, para que possíveis exceções não sejam exibidas:
class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try {
Macros auxiliares STATIC_ASSERT_NOEXCEPT e STATIC_ASSERT_NOT_NOEXCEPT
Infelizmente, as macros ENSURE_NOEXCEPT_STATEMENT e ENSURE_NOT_NOEXCEPT_STATEMENT podem ser usadas apenas para instruções / declarações, mas não para expressões que retornam um valor. I.e. você não pode escrever com ENSURE_NOEXCEPT_STATEMENT assim:
auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params));
Portanto, ENSURE_NOEXCEPT_STATEMENT não pode ser usado, por exemplo, em loops em que você geralmente precisa escrever algo como:
for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
e você precisa se certificar de que as chamadas get_first()
, get_next()
, bem como a atribuição de novos valores para eu não lançem uma exceção.
Para combater essas situações, as macros STATIC_ASSERT_NOEXCEPT e STATIC_ASSERT_NOT_NOEXCEPT foram gravadas, atrás das quais apenas static_assert s estão ocultos e nada mais. Usando essas macros, posso obter o resultado necessário de alguma maneira (a compilação desse fragmento em particular não foi verificada):
STATIC_ASSERT_NOEXCEPT(something.get_first()); STATIC_ASSERT_NOEXCEPT(something.get_first().get_next()); STATIC_ASSERT_NOEXCEPT(std::declval<decltype(something.get_first())>() = something.get_first().get_next()); for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
Obviamente, essa não é a melhor solução, porque isso leva à duplicação de código e aumenta o risco de sua "fluência" com manutenção adicional. Mas, como primeiro passo, essas macros simples se mostraram úteis.
Biblioteca Noexcept-ctcheck
Quando compartilhei essa experiência no meu blog e no Facebook, recebi uma proposta para organizar os desenvolvimentos acima em uma biblioteca separada. O que foi feito: o github agora tem uma pequena biblioteca somente de cabeçalho noexcept-compile-time-check (ou noexcept-ctcheck, se você salvar em letras) . Portanto, todas as opções acima você pode pegar e experimentar. É verdade que os nomes das macros são um pouco mais longos do que o usado no artigo. I.e. NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT em vez de ENSURE_NOEXCEPT_STATEMENT.
O que não deu no noexcept-ctcheck (ainda?)
Há um desejo de criar a macro ENSURE_NOEXCEPT_EXPRESSION, que pode ser usada assim:
auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params));
Em uma primeira aproximação, ele pode se parecer com isso:
#define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }())
Mas há vagas suspeitas de que existem algumas armadilhas nas quais não pensei. Em geral, as mãos ainda não chegaram a ENSURE_NOEXCEPT_EXPRESSION :(
E se você sonhar?
Meu antigo sonho é obter um bloco noexcept em C ++, no qual o próprio compilador verifica se há exceções de lançamento e emite avisos se houver exceções. Parece-me que isso tornaria mais fácil escrever código com exceção de segurança. E não apenas nos casos óbvios mencionados acima (swap, operadores de movimentação, destruidores). Por exemplo, um bloco noexcept poderia ajudar nessa situação:
void modify_some_complex_data() {
Aqui, para a correção do código, é muito importante que as ações executadas nos blocos noexcept não gerem exceções. E se o compilador puder rastrear isso, será uma ajuda séria para o desenvolvedor.
Mas talvez um bloco de exceção seja apenas um caso especial de um problema mais geral. A saber: verificar as expectativas do programador de que algum bloco de código tenha certas propriedades. Seja a ausência de exceções, a ausência de efeitos colaterais, a ausência de recursão, corridas de dados, etc.
Reflexões sobre esse assunto, há alguns anos, levaram à idéia de atributos implícitos e esperados . Essa idéia não foi além da postagem do blog, porque enquanto ela se afasta dos meus interesses e oportunidades atuais. Mas, de repente, será interessante para alguém e alguém pressionará para criar algo mais viável.
Conclusão
Neste artigo, tentei falar sobre minha experiência na simplificação da gravação de código com exceção de segurança. Usar macros, é claro, não torna o código mais bonito e compacto. Mas funciona. E mesmo essas macros primitivas aumentam significativamente o coeficiente do meu sono reparador. Portanto, se alguém não pensou em como controlar o conteúdo de seus próprios métodos / funções, exceto talvez, este artigo talvez o inspire a pensar sobre este tópico.
E se alguém encontrasse uma maneira de simplificar sua vida ao escrever código sem exceção, seria interessante saber qual é esse método, em que ajuda e em que não. E quão satisfeito você está com o que usa.