
Algumas semanas atrás, foi a principal conferência no mundo C ++ - CPPCON .
Cinco dias consecutivos das 8h às 22h, houve relatos. Programadores de todas as religiões discutiram o futuro do C ++, bicicletas envenenadas e pensaram em como tornar o C ++ mais fácil.
Surpreendentemente, muitos relatórios foram dedicados ao tratamento de erros. Abordagens bem estabelecidas não permitem atingir o desempenho máximo ou podem gerar folhas de código.
Quais inovações nos esperam em C ++ 2a?
Pouco de teoria
Convencionalmente, todas as situações errôneas do programa podem ser divididas em 2 grandes grupos:
- Erros fatais.
- Erros não fatais ou esperados.
Erros fatais
Depois deles, não faz sentido continuar a execução.
Por exemplo, isso está desreferenciando um ponteiro nulo, passando pela memória, dividindo por 0 ou violando outros invariantes no código. Tudo o que precisa ser feito quando ocorrerem é fornecer o máximo de informações sobre o problema e concluir o programa.
Em c ++ demais já existem maneiras suficientes de concluir o programa:
As bibliotecas estão começando a aparecer para coletar dados sobre falhas ( 1 , 2 , 3 ).
Erros não fatais
Esses são erros fornecidos pela lógica do programa. Por exemplo, erros ao trabalhar com a rede, converter uma sequência inválida em um número, etc. A aparência de tais erros no programa está na ordem das coisas. Para o processamento, existem várias táticas geralmente aceitas em C ++.
Vamos falar sobre eles em mais detalhes usando um exemplo simples:
Vamos tentar escrever uma função void addTwo()
usando abordagens diferentes para o tratamento de erros.
A função deve ler 2 linhas, convertê-las em int
e imprimir a soma. Precisa lidar com erros de IO, estouro e conversão para número. Omitirei detalhes de implementação desinteressantes. Vamos considerar três abordagens principais.
1. Exceções
// // IO std::runtime_error std::string readLine(); // int // std::invalid_argument int parseInt(const std::string& str); // a b // std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } }
Exceções em C ++ permitem que você lide com erros centralmente sem
desnecessário
,
mas você tem que pagar por isso com um monte de problemas.
- a sobrecarga envolvida no tratamento de exceções é bastante grande; muitas vezes você não pode lançar exceções.
- é melhor não lançar exceções de construtores / destruidores e observar RAII.
- pela assinatura da função, é impossível entender qual exceção pode sair da função.
- o tamanho do arquivo binário aumenta devido a um código de suporte de exceção adicional.
2. Códigos de retorno
Abordagem clássica herdada de C.
bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; }
Não parece muito bom?
- Você não pode retornar o valor real de uma função.
- É muito fácil esquecer o tratamento do erro (na última vez em que você verificou o código de retorno de printf?).
- Você deve escrever o código de tratamento de erros ao lado de cada função. Esse código é mais difícil de ler.
Usar C ++ 17 e C ++ 2a corrigirá todos esses problemas em sequência.
3. C ++ 17 e nodiscard
O nodiscard
no C ++ 17.
Se você o especificar antes da declaração da função, a ausência de verificação do valor de retorno causará um aviso do compilador.
[[nodiscard]] bool doStuff(); doStuff();
Você também pode especificar o nodiscard
para uma classe, estrutura ou classe de enumeração.
Nesse caso, a ação do atributo se estende a todas as funções que retornam valores do tipo rotulado nodiscard
.
enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir();
Não fornecerei código com nodiscard
.
C ++ 17 std :: opcional
No C ++ 17, std::optional<T>
.
Vamos ver como o código fica agora.
std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; }
Você pode remover argumentos de entrada e saída das funções e o código ficará mais limpo.
No entanto, estamos perdendo informações de erro. Não ficou claro quando e o que deu errado.
Você pode substituir std::optional
por std::variant<ResultType, ValueType>
.
O significado do código é o mesmo que com std::optional
, mas mais complicado.
C ++ 2a e std :: esperado
std::expected<ResultType, ErrorType>
- um tipo de modelo especial , provavelmente cairá no padrão incompleto mais próximo.
Possui 2 parâmetros.
Como isso difere da variant
usual? O que o torna especial?
std::expected
será uma mônada .
É proposto oferecer suporte a um monte de operações no std::expected
catch_error
como em uma mônada: map
, catch_error
, bind
, unwrap
, return
e then
.
Usando essas funções, você pode encadear chamadas de função em uma cadeia.
getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; );
Suponha que tenhamos funções com o retorno de std::expected
.
std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b);
Abaixo está apenas o pseudo-código; ele não pode ser forçado a funcionar em nenhum compilador moderno.
Você pode tentar emprestar da Haskell a sintaxe do para operações de gravação em mônadas. Por que não permitir:
std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) }
Alguns autores sugerem esta sintaxe:
try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; }
O compilador converte automaticamente esse bloco de código em uma sequência de chamadas de função. Se em algum momento a função retornar não o que é esperado dela, a cadeia de computação será interrompida. Sim, e como tipo de erro, você pode usar os tipos de exceção já existentes no padrão: std::runtime_error
, std::out_of_range
, etc.
Se você pode projetar bem a sintaxe, então std::expected
permitirá que você escreva um código simples e eficiente.
Conclusão
Não existe uma maneira ideal de lidar com erros. Até recentemente, em C ++ havia quase todos os métodos possíveis de manipulação de erros, exceto mônadas.
No C ++ 2a, todos os métodos possíveis provavelmente aparecerão.
O que ler e ver no tópico
- Proposta real .
- Discurso sobre std :: esperado com CPPCON .
- Andrei Alexandrescu sobre std :: esperado em C ++ na Rússia .
- Discussão mais ou menos recente da proposta no Reddit .