É estranho que em Habrt ainda não tenha sido mencionada uma proposta clamorosa para o padrão C ++ chamada "Exceções determinísticas de sobrecarga zero". Corrigindo essa omissão irritante.
Se você está preocupado com a sobrecarga de exceções, ou teve que compilar o código sem suporte a exceções, ou apenas se perguntando o que acontecerá com o tratamento de erros no C ++ 2b (uma referência a uma publicação recente ), peço cat. Você está esperando por um aperto de tudo o que agora pode ser encontrado sobre o tópico, e algumas pesquisas.
A discussão abaixo será conduzida não apenas sobre exceções estáticas, mas também sobre propostas relacionadas ao padrão e sobre todas as outras formas de lidar com erros. Se você foi aqui para olhar a sintaxe, aqui está:
double safe_divide(int x, int y) throws(arithmetic_error) { if (y == 0) { throw arithmetic_error::divide_by_zero; } else { return as_double(x) / y; } } void caller() noexcept { try { cout << safe_divide(5, 2); } catch (arithmetic_error e) { cout << e; } }
Se o tipo específico de erro não for importante / desconhecido, você poderá simplesmente usar throws
e catch (std::error e)
.
Bom saber
std::optional
e std::expected
Vamos decidir que o erro que possa surgir na função não seja "fatal" o suficiente para gerar uma exceção a ela. Tradicionalmente, as informações de erro são retornadas usando um parâmetro out. Por exemplo, o Filesystem TS oferece vários recursos semelhantes:
uintmax_t file_size(const path& p, error_code& ec);
(Não lance uma exceção devido ao fato de o arquivo não ter sido encontrado?) No entanto, o processamento do código de erro é complicado e propenso a erros. É fácil esquecer o código de erro para verificar. Os estilos de código modernos proíbem o uso de parâmetros de saída; em vez disso, é recomendável retornar uma estrutura contendo o resultado inteiro.
Há algum tempo, o Boost oferece uma solução elegante para lidar com esses erros "não fatais" que podem ocorrer em determinados cenários no programa correto:
expected<uintmax_t, error_code> file_size(const path& p);
O tipo expected
é semelhante à variant
, mas fornece uma interface conveniente para trabalhar com o "resultado" e o "erro". Por padrão, o resultado expected
é armazenado no expected
. A implementação file_size
pode ser algo como isto:
file_info* info = read_file_info(p); if (info != null) { uintmax_t size = info->size; return size;
Se a causa do erro não for interessante para nós, ou o erro puder consistir apenas na "ausência" do resultado, o optional
poderá ser usado:
optional<int> parse_int(const std::string& s); optional<U> get_or_null(map<T, U> m, const T& key);
No C ++ 17 do Boost, o opcional chegou ao padrão (sem suporte para optional<T&>
); em C ++ 20, eles podem adicionar o esperado (essa é apenas a proposta, obrigado RamzesXI pela correção).
Contratos
Os contratos (que não devem ser confundidos com os conceitos) são uma nova maneira de impor restrições aos parâmetros de função, adicionados no C ++ 20. 3 anotações adicionadas:
- espera verificações de parâmetros de função
- garante a verificação do valor de retorno da função (aceita como argumento)
- assert - um substituto civilizado para a macro assert
double unsafe_at(vector<T> v, size_t i) [[expects: i < v.size()]]; double sqrt(double x) [[expects: x >= 0]] [[ensures ret: ret >= 0]]; value fetch_single(key e) { vector<value> result = fetch(vector<key>{e}); [[assert result.size() == 1]]; return v[0]; }
Você pode configurar por quebra de contrato:
- Comportamento Indefinido Chamado ou
- Ele verificou e chamou a saída do usuário, após o que
std::terminate
É impossível continuar executando o programa após a quebra do contrato, porque os compiladores usam garantias dos contratos para otimizar o código de função. Se houver a menor dúvida de que o contrato será cumprido, vale a pena adicionar uma verificação adicional.
std :: error_code
A biblioteca <system_error>
, adicionada no C ++ 11, permite padronizar o tratamento de códigos de erro no seu programa. std :: error_code consiste em um código de erro do tipo int
e um ponteiro para o objeto de alguma classe descendente std :: error_category . Esse objeto, de fato, desempenha o papel de uma tabela de funções virtuais e determina o comportamento de um dado std::error_code
.
Para criar seu std::error_code
, você deve definir sua std::error_category
descendente std::error_category
e implementar métodos virtuais, o mais importante dos quais é:
virtual std::string message(int c) const = 0;
Você também deve criar uma variável global para sua std::error_category
. O tratamento de erros usando o error_code + esperado é algo como isto:
template <typename T> using result = expected<T, std::error_code>; my::file_handle open_internal(const std::string& name, int& error); auto open_file(const std::string& name) -> result<my::file> { int raw_error = 0; my::file_handle maybe_result = open_internal(name, &raw_error); std::error_code error{raw_error, my::filesystem_error}; if (error) { return unexpected{error}; } else { return my::file{maybe_result}; } }
É importante que em std::error_code
valor 0 signifique nenhum erro. Se esse não for o caso dos seus códigos de erro, antes de converter o código de erro do sistema em std::error_code
, você deve substituir o código 0 por SUCCESS e vice-versa.
Todos os códigos de erro do sistema são descritos em errc e system_category . Se, em um certo estágio, o encaminhamento manual dos códigos de erro se tornar muito sombrio, você poderá sempre quebrar o código de erro na std::system_error
e jogá-lo fora.
Movimento destrutivo / Trivialmente relocável
Você precisa criar outra classe de objetos que possuam alguns recursos. Provavelmente, você desejará torná-lo não copiável, mas móvel, porque os objetos imóveis são inconvenientes para trabalhar (antes do C ++ 17, eles não podiam ser retornados de uma função).
Mas aqui está o problema: em qualquer caso, o objeto movido precisa ser excluído. Portanto, é necessário um estado especial de "movido de", ou seja, um objeto "vazio" que não exclui nada. Acontece que cada classe C ++ deve ter um estado vazio, ou seja, é impossível criar uma classe com uma invariante (garantia) de correção, do construtor ao destruidor. Por exemplo, não é possível criar a classe open_file
correta de um arquivo aberto durante toda a sua vida útil. É estranho observar isso em um dos poucos idiomas que usam ativamente o RAII.
Outro problema é o zeramento de objetos antigos quando a movimentação adiciona uma sobrecarga: o preenchimento de std::vector<std::unique_ptr<T>>
pode ser até duas vezes mais lento que std::vector<T*>
devido ao monte de zeragem de ponteiros antigos ao se mover , seguido pela remoção de manequins.
Os desenvolvedores de C ++ há muito tempo lambiam o Rust, onde os destruidores não são chamados em objetos realocados. Esse recurso é chamado de movimento destrutivo. Infelizmente, a proposta Trivially relocatable não oferece para adicioná-lo ao C ++. Mas o problema de sobrecarga será resolvido.
Uma classe é considerada relocável de maneira trivial se duas operações: mover e excluir o objeto antigo forem equivalentes a memcpy do objeto antigo para o novo. O objeto antigo não é excluído, os autores chamam de "solte-o no chão".
Um tipo é Trivialmente relocável do ponto de vista do compilador se uma das seguintes condições (recursivas) for verdadeira:
- É trivialmente móvel + trivialmente destrutível (por exemplo, estrutura
int
ou POD) - Esta é a classe marcada com o atributo
[[trivially_relocatable]]
- Esta é uma classe da qual todos os membros são trivialmente relocáveis.
Você pode usar essas informações com std::uninitialized_relocate
, que executa o movimento init + delete da maneira usual ou acelerado, se possível. Sugere-se marcar como [[trivially_relocatable]]
maioria dos tipos de biblioteca padrão, incluindo std::string
, std::vector
, std::unique_ptr
. Overhead std::vector<std::unique_ptr<T>>
com isso em mente A proposta desaparecerá.
O que há de errado com exceções agora?
O mecanismo de exceção C ++ foi desenvolvido em 1992. Várias opções de implementação foram propostas. Desses, foi selecionado um mecanismo de tabela de exceção que garante a ausência de uma sobrecarga para o caminho principal da execução do programa. Porque, desde o momento de sua criação, assumiu-se que exceções deveriam ser lançadas muito raramente .
Desvantagens de exceções dinâmicas (ou seja, regulares):
- No caso da exceção lançada, a sobrecarga é em média de 10.000 a 100.000 ciclos de CPU e, na pior das hipóteses, pode atingir a ordem de milissegundos
- Tamanho do arquivo binário aumentado em 15-38%
- Incompatibilidade com a interface de programação C
- Exceção implícita ao suporte em todas as funções, exceto
noexcept
. Uma exceção pode ser lançada quase em qualquer lugar do programa, mesmo quando o autor da função não espera.
Devido a essas deficiências, o escopo das exceções é significativamente limitado. Quando as exceções não podem ser aplicadas:
- Onde o determinismo é importante, ou seja, onde é inaceitável que o código "às vezes" funcione 10, 100, 1000 vezes mais lento que o normal
- Quando eles não são suportados na ABI, por exemplo, em microcontroladores
- Quando grande parte do código é escrita em C
- Em empresas com uma grande carga de código legado ( Google Style Guide , Qt ). Se houver pelo menos uma função não protegida por exceção no código, de acordo com a lei da maldade, uma exceção será lançada mais cedo ou mais tarde e criará um bug
- Nas empresas que contratam programadores que não têm idéia sobre segurança de exceção
Segundo pesquisas, nos locais de trabalho de 52% (!) Desenvolvedores, as exceções são proibidas pelas regras corporativas.
Mas as exceções são parte integrante do C ++! Ao incluir o -fno-exceptions
, os desenvolvedores perdem a capacidade de usar uma parte significativa da biblioteca padrão. Isso incita ainda mais as empresas a plantar suas próprias "bibliotecas padrão" e, sim, inventar sua própria classe de cadeias.
Mas este não é o fim. Exceções são a única maneira padrão de cancelar a criação de um objeto no construtor e gerar um erro. Quando desativados, uma abominação, como a inicialização em duas fases, é exibida. Os operadores também não podem usar códigos de erro; portanto, eles são substituídos por funções como assign
.
Proposta: exceções do futuro
Novo mecanismo de transferência de exceção
Herb Sutter em P709 descreveu um novo mecanismo de transferência de exceções. Em princípio, a função retorna std::expected
, no entanto, em vez de um discriminador separado do tipo bool
, que junto com o alinhamento ocupará até 8 bytes na pilha, essa informação é transmitida de maneira mais rápida, por exemplo, para Carry Flag.
As funções que não tocam em CF (a maioria) terão a oportunidade de usar exceções estáticas gratuitamente - tanto no caso de um retorno normal quanto no caso de lançar uma exceção! As funções forçadas a salvá-lo e restaurá-lo receberão uma sobrecarga mínima e ainda serão mais rápidas que o std::expected
e qualquer código de erro comum.
As exceções estáticas são assim:
int safe_divide(int i, int j) throws(arithmetic_errc) { if (j == 0) throw arithmetic_errc::divide_by_zero; if (i == INT_MIN && j == -1) throw arithmetic_errc::integer_divide_overflows; return i / j; } double foo(double i, double j, double k) throws(arithmetic_errc) { return i + safe_divide(j, k); } double bar(int i, double j, double k) { try { cout << foo(i, j, k); } catch (erithmetic_errc e) { cout << e; } }
Na versão alternativa, propõe-se obrigar a palavra-chave try
na mesma expressão que a chamada da função throws
: try i + safe_divide(j, k)
. Isso reduzirá o número de casos de uso de funções throws
no código que não é seguro para exceções quase zero. De qualquer forma, diferentemente das exceções dinâmicas, o IDE poderá, de alguma forma, destacar expressões que geram exceções.
O fato de a exceção lançada não ser armazenada separadamente, mas ser colocada diretamente no lugar do valor retornado, impõe restrições ao tipo de exceção. Primeiro, ele deve ser trivialmente realocável. Em segundo lugar, seu tamanho não deve ser muito grande (mas pode ser algo como std::unique_ptr
), caso contrário, todas as funções reservarão mais espaço na pilha.
status_code
A biblioteca <system_error2>
, desenvolvida por Niall Douglas, conterá status_code<T>
- "novo, melhor" error_code
. As principais diferenças de error_code
:
status_code
- um tipo de modelo que pode ser usado para armazenar quase todos os códigos de erro concebíveis (junto com um ponteiro para status_code_category
), sem usar exceções estáticasT
deve ser trivialmente relocável e copiável (este último, IMHO, não deve ser obrigatório). Ao copiar e excluir, as funções virtuais são chamadas de status_code_category
status_code
pode armazenar não apenas dados de erro, mas também informações adicionais sobre uma operação concluída com êxito- A função "virtual"
code.message()
não retorna std::string
, mas string_ref
é um tipo de string bastante pesado, que é um std::string_view
"possivelmente proprietário" std::string_view
. Lá você pode string_view
ou string
, ou std::shared_ptr<string>
, ou alguma outra maneira maluca de possuir uma string. Niall afirma que #include <string>
tornaria o cabeçalho <system_error2>
inaceitavelmente "pesado"
Em seguida, errored_status_code<T>
é inserido - um wrapper sobre status_code<T>
com o seguinte construtor:
errored_status_code(status_code<T>&& code) [[expects: code.failure() == true]] : code_(std::move(code)) {}
erro
O tipo de exceção padrão ( throws
sem tipo), bem como o tipo básico de exceções para as quais todas as outras são convertidas (como std::exception
), é error
. É definido algo como isto:
using error = errored_status_code<intptr_t>;
Ou seja, error
é um status_code
"erro", no qual o valor ( value
) é colocado em 1 ponteiro. Como o mecanismo status_code_category
garante exclusão, movimento e cópia corretos, teoricamente, qualquer estrutura de dados pode ser salva por error
. Na prática, esta será uma das seguintes opções:
- Inteiros (int)
std::exception_handle
, ou seja, um ponteiro para uma exceção dinâmica lançadastatus_code_ptr
, ou seja, unique_ptr
para um status_code<T>
arbitrário status_code<T>
.
O problema é que o caso 3 não está planejado para dar a oportunidade de retornar o error
ao status_code<T>
. A única coisa que você pode fazer é obter a message()
status_code<T>
compactado status_code<T>
. Para poder recuperar o valor novamente em error
, ative-o como uma exceção dinâmica (!). Em seguida, pegue e envolva-o em error
. Em geral, Niall acredita que apenas códigos de erro e mensagens de string devem ser armazenados com error
, o que é suficiente para qualquer programa.
Para distinguir entre diferentes tipos de erros, propõe-se usar o operador de comparação "virtual":
try { open_file(name); } catch (std::error e) { if (e == filesystem_error::already_exists) { return; } else { throw my_exception("Unknown filesystem error, unable to continue"); } }
O uso de vários blocos catch ou dynamic_cast
para selecionar o tipo de exceção falhará!
Interação com exceções dinâmicas
Uma função pode ter uma das seguintes especificações:
noexcept
: não lança exceçõesthrows(E)
: lança apenas exceções estáticas- (nada): lança apenas exceções dinâmicas
throws
implica noexcept
. Se uma exceção dinâmica é lançada a partir de uma função "estática", é envolto em error
. Se uma exceção estática for lançada de uma função "dinâmica", ela será status_error
em uma exceção status_error
. Um exemplo:
void foo() throws(arithmetic_errc) { throw erithmetic_errc::divide_by_zero; } void bar() throws {
Exceções em C ?!
A proposta prevê a adição de exceções a um dos futuros padrões C, e essas exceções serão compatíveis com ABI com exceções estáticas em C ++. Uma estrutura semelhante ao std::expected<T, U>
, o usuário precisará declarar independentemente, embora a redundância possa ser removida usando macros. A sintaxe consiste em (por simplicidade, assumiremos isso) as palavras-chave falham, falham, capturam.
int invert(int x) fails(float) { if (x != 0) return 1 / x; else return failure(2.0f); } struct expected_int_float { union { int value; float error; }; _Bool failed; }; void caller() { expected_int_float result = catch(invert(5)); if (result.failed) { print_error(result.error); return; } print_success(result.value); }
Ao mesmo tempo, em C ++ também será possível chamar funções de fails
de C, declarando-as em blocos extern C
. Assim, em C ++, haverá toda uma galáxia de palavras-chave para trabalhar com exceções:
throw()
- removido em C ++ 20noexcept
- especificador de função, a função não lança exceções dinâmicasnoexcept(expression)
- especificador de função, a função não lança exceções dinâmicas fornecidasnoexcept(expression)
- Uma expressão noexcept(expression)
exceções dinâmicas?throws(E)
- especificador de função, a função lança exceções estáticasthrows
= throws(std::error)
fails(E)
- uma função importada de C lança exceções estáticas
Assim, em C ++ eles trouxeram (ou melhor, entregaram) um carrinho de novas ferramentas para tratamento de erros. Em seguida, surge uma pergunta lógica:
Quando usar o que?
Direção geral
Os erros são divididos em vários níveis:
- Erros do programador. Processado usando contratos. Eles levam à coleta de logs e ao encerramento do programa, de acordo com o conceito de fail-fast . Exemplos: ponteiro nulo (quando isso é inválido); divisão por zero; erros de alocação de memória não previstos pelo programador.
- Erros fatais fornecidos pelo programador. Jogado fora um milhão de vezes menos que o retorno normal de uma função, o que justifica o uso de exceções dinâmicas. Normalmente, nesses casos, você precisa reiniciar todo o subsistema do programa ou cometer um erro ao executar a operação. Exemplos: perda repentina da conexão com o banco de dados; erros de alocação de memória fornecidos pelo programador.
- Erros recuperáveis quando algo impedia a função de concluir sua tarefa, mas a função de chamada pode saber o que fazer com ela. Manipulado por exceções estáticas. Exemplos: trabalhe com o sistema de arquivos; outros erros de entrada / saída (IO); Dados incorretos do usuário
vector::at()
. - A função concluiu com êxito sua tarefa, embora com um resultado inesperado.
std::optional
, std::expected
, std::variant
. Exemplos: stoi()
; vector::find()
; map::insert
.
Na biblioteca padrão, é mais confiável abandonar completamente o uso de exceções dinâmicas para tornar legal a compilação "sem exceções".
errno
As funções que usam errno
para trabalhar rápida e facilmente com os códigos de erro C e C ++ devem ser substituídas por throws(std::errc)
fails(int)
e throws(std::errc)
, respectivamente. Por algum tempo, as versões antiga e nova das funções da biblioteca padrão coexistirão, e a antiga será declarada obsoleta.
Falta de memória
Os erros de alocação de memória são manipulados pelo gancho global new_handler
, que pode:
- Elimine a falta de memória e continue a execução
- Lançar uma exceção
- Programa de falha
Agora std::bad_alloc
lançado por padrão. É recomendável chamar std::terminate()
por padrão. Se você precisar do comportamento antigo, substitua o manipulador pelo que você precisa no início de main()
.
Todas as funções existentes da biblioteca padrão não serão noexcept
e noexcept
o programa quando std::bad_alloc
. Ao mesmo tempo, novas APIs como vector::try_push_back
serão adicionadas, o que permite erros de alocação de memória.
logic_error
Exceções std::logic_error
, std::domain_error
, std::invalid_argument
, std::length_error
, std::out_of_range
, std::future_error
reportam uma violação de uma pré-condição de função. O novo modelo de erro deve usar contratos. Os tipos de exceções listados não serão preteridos, mas quase todos os casos de uso na biblioteca padrão serão substituídos por [[expects: …]]
.
Status atual da proposta
A proposta está agora em um estado de rascunho. Ele já mudou bastante e ainda pode mudar muito. Como alguns desenvolvimentos não conseguiram ser publicados, a API proposta <system_error2>
não <system_error2>
totalmente relevante.
A proposta está descrita em 3 documentos:
- P709 - documento original do brasão de armas da Sutter
- P1095 - Exceções determinadas na visão de Niall Douglas, alguns momentos alterados, compatibilidade de idioma C adicionada
- P1028 - API da implementação de teste de
std::error
Atualmente, não há compilador que suporte exceções estáticas. Portanto, ainda não é possível fazer seus benchmarks.
C++23. , , , C++26, , , .
Conclusão
, , . , . .
, ^^