Tag Archive | rust

Eu não confio no seu julgamento se você não critica sua linguagem de programação

A lógica por trás de tal filosofia é bem simples. Se sua linguagem de programação não possui defeitos, então não há nada para mudar nela, pois ela é perfeita, exatamente como está, imutável, nada precisa mudar. Entretanto, e essa é a parte da argumentação que é menos baseada em consequências lógicas e mais baseada em observações ao meu redor, ainda está para ocorrer o momento em que eu veja um entusiasta de sua linguagem de programação que considere negativo o lançamento de uma nova versão de sua linguagem de programação. Cada uma das mudanças que culminou no lançamento de uma nova versão de sua linguagem de programação era uma carência ou um defeito que existia em sua antiga versão, e os “fãs” só irão reconhecê-lo uma vez que o defeito é corrigido, pois sua linguagem é perfeita, sagrada, livre de questionamentos, um tabu quando se menciona a ideia de defeitos.

Pois bem, eu não acho essa posição de sua parte nenhum pouco honesta, e eu quis fazer esse texto para tentar fazer você refletir um pouco a respeito. Outro motivo é que eu estava há muito tempo sem escrever e esse foi um texto fácil para mim, que fluiu da minha mente para “o papel” encontrando nenhuma barreira ou barreiras imperceptíveis. Eu perdi muito pouco tempo para fazê-lo. É um texto de mera opinião.

A ideia de fazer esse texto me veio após perder bastante tempo para escrever a pauta para um podcast que me convidaram a gravar. O tema do podcast seria a linguagem Rust, e eu, na minha mentalidade de blogueiro que ainda não sabe montar pautas, dediquei 4 páginas da pauta só para elaborar o quão C++ é uma linguagem ruim. Agora você precisa entender que a linguagem C++ foi minha linguagem favorita por 6 anos de tal forma que simplesmente não havia espaço para carinho a outras linguagens de programação, e que eu dediquei muito tempo de minha vida só para entender como eu poderia defender essa linguagem. Hoje em dia, C++ não perdeu meu carinho e ela ainda é minha solução favorita para metade dos problemas que resolvo. Ainda assim, 90% do tempo que dediquei para montar a pauta, foi para criticar C++, e em momento nenhum deixei espaço para que o consumidor daquela pauta/obra pudesse imaginar que eu tenho um apego tão grande por essa linguagem.

Acho que uma boa forma de terminar esse texto é ressaltar que sua linguagem só evolui se você corrigir seus problemas, e isso só vai acontecer uma vez que seus problemas sejam reconhecidos. A “linguagem perfeita” é um termo que só é usado por programadores imaturos, grupo do qual um dia eu também já fiz parte.

Monads

Faz algum tempo desde a última vez que escrevo sobre algum padrão de projeto. O último texto que lembro foi o texto sobre CRTP, e nesse tempo meu conhecimento sobre programação aumentou, minhas habilidades comunicativas aumentaram e eu passei a escrever textos melhores. O texto que fiz sobre CRTP, nem acho importante. Entretanto, decidi fazer um texto para explicar monads, pois existe toda essa tradição de que, ao compreender o que é monad, você atinge uma epifania satisfatoriamente envolvente, você sente um desejo incontrolável de tentar compartilhar esse conhecimento com toda a humanidade, e você FALHA, mas, até pior que isso, seu manual é diferente e as pessoas vão perder até mais tempo tentando aprender o que é monad, pois agora há até mais textos confusos que acumulamos.

Eu, é claro, demorei muito tempo para aprender monads, apesar de estar usando monads há bastante tempo. Quando finalmente aprendi o que é monad, percebi o quão simples é esse conceito. Devo essa compreensão ao Douglas Crockford.

Introdução

All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.

Entendeu? Eu também não, e esse tipo de explicação era uma das razões para eu ter demorado a aprender.

Uma coisa importante para prosseguir com a explicação, é deixar claro os termos utilizados. E o primeiro termo que pretendo deixar claro é o de valor. Em programação funcional pura, não é tão raro assim evitar o uso do termo variável e usar, no lugar, o termo associar (binding em inglês), para indicar que um nome está se referindo a algum valor. Quando você se refere a variável, talvez implicitamente você assuma que você pode mudar o valor dessa variável, mas nem sempre é essa a mensagem que queremos comunicar. Há também o termo objeto, do paradigma de programação orientada a objetos, que podemos usar no lugar do termo variável, mas o termo objeto também carrega  outras informações que podem não ser necessárias para a explicação que estamos tentando passar. Por essas razões, eu vou, durante o texto, tentar usar o termo valor, mas se você substituir esse termo por objeto ou variável, ainda é provável que o texto continue fazendo sentido.

Há também o termo função, que em programação pode acabar tendo um significado diferente. Em programação, há o termo subrotina, que não equivale ao termo de função na matemática. Entretanto, algumas linguagens de programação usam o termo função para se referir ao conceito de subrotina, criando uma situação de falso cognato. Para “diminuir” a confusão, passamos a chamar de funções puras, as funções que possuíam o mesmo significado que as funções possuem em matemática. Essa curiosidade não é tão importante para o entendimento de monads, mas é bom que você comece a consumir algumas informações relacionadas para estar estimulado o suficiente quando a explicação de monads aparecer.

Ainda no tópico de funções, temos a diferença de funções membros e funções não-membros. Funções membros são funções que recebem o this/self, que fazem parte de alguma classe e não são métodos estáticos, funções que chamamos de métodos na programação orientada a objetos. Funções não-membros são funções livres, que não fazem parte de nenhuma classe. Quando eu me referir a uma função que recebe a e b como argumentos, as duas situações exemplificadas no seguinte código são interpretações válidas:

Nos dois casos, temos uma função foo que recebe a e b como argumentos. Há até esforços para tornar a sintaxe de chamada de funções em C++ mais uniforme. A linguagem Rust está ligeiramente à frente nessa corrida, enquanto outras linguagens já chegaram lá.

Você pode enxergar monad como um padrão de projeto, e, dentro desse padrão de projeto, há o “objeto” que de fato representa o monad. Assim como ocorre no padrão Singleton, onde o mesmo termo, Singleton, se refere (1) ao padrão de projetos e a (1) um objeto que respeita algumas características. Estou citando o padrão Singleton, porque esse é, pela minha experiência, o padrão mais famoso e mais fácil de entender. A parte importante para manter em mente é que monad é um valor, mas, em alguns momentos, pode ser que eu me refira a monad como um padrão de projeto.

Valores embrulhados

Em alguns cantos na terra da programação, podemos encontrar valores que são embrulhados. Por exemplo, temos a classe Integer, na linguagem Java, que embrulha um valor primitivo int. Temos também, em C++, a classe auto_ptr<T>, que embrulha um valor do tipo T. Instâncias dessas classes não são monads.

Valores embrulhados por outro valor são isolados, tornando-se acessíveis somente a partir de uma interface. Eles não podem ser acessados diretamente, pois estão embrulhados em outro valor, seja lá qual for o propósito. No caso da classe Integer, você precisa usar a função intValue para acessar o valor embrulhado. No caso da classe auto_ptr<T>, você precisa usar a função que sobrecarrega o operador de desreferenciamento da classe.

Monads também embrulham valores. Monads são valores que embrulham outros valores e apresentam três propriedades. Entretanto, eu só vou informá-las ao final do texto.

Função unit e função bind

Unit é uma função que recebe como argumento o valor a ser embrulhado e retorna o monad. É também conhecido como construtor em outros locais. Seria o “construtor” do monad.

Bind é uma função que recebe dois argumentos, um monad e uma função que tenha a “mesma assinatura” de unit, e retorna um novo monad. Usei a expressão “mesma assinatura” entre aspas, pois, na verdade, a situação não precisa ser tão estrita assim. O tipo de retorno pode ser um monad diferente, mas o argumento de entrada precisa ser do mesmo tipo ou compatível.

Em um monad, deve existir uma forma de acessar o valor embrulhado através da função bind, mesmo que essa não seja a única forma de acessar o valor, e mesmo que a função receba outro nome que não seja bind. Daí que encontramos algumas discussões usando termos como “monadic operations“.

A ideia por trás dessa estrutura é a componibilidade. Talvez não seja tão incrível para eu ou você, pois estamos acostumados a ter outras ferramentas disponíveis, mas parece ser um conceito incrível para os programadores da comunidade de programação funcional pura.

Em Haskell, monads costumam ser usados para isolar comportamentos que não são livres de efeitos colaterais, como I/O. Rust, uma linguagem que possui “;”, não precisa de monads, mas o núcleo de seu sistema da tratamentos de erros é feito em cima das ideias de monads.

Alguns monads

Maybe

Maybe é um monad, em alguns locais recebendo o nome de Option ou optional, e às vezes nem sequer obedecendo as propriedades de um monad. O propósito de Maybe é simples: possivelmente armazenar um valor. Você pode encarar esse monad como uma abstração que lhe permite expressar a semântica de valores opcionais, que em C são representados através de ponteiros que possivelmente possuem o valor NULL, ou em Java, onde qualquer objeto pode ser null.

O legal desse monad é que você ganha segurança, pois verificar pela existência do valor para então acessá-lo não acontece. É mais seguro, porque, no modelo antigo, era possível você tentar acessar o valor esquecendo de verificar antes se ele existia, esquecer do if. Com o padrão de monad/Maybe, você só usa a função bind que já recebe o valor “extraído” e ela só é chamada se o valor for diferente de None.

A verdade é que esse monad fica bem mais legal se houver suporte a nível de linguagem para exhaustive pattern matching, como ocorre em Rust. Em Rust, a abstração que equivale a esse monad é a abstração Option.

Na verdade, o exemplo de código anterior usa um bind “não-conformante”, pois a função/closure passada como argumento não retorna outro monad, mas escrevi mesmo assim para ser breve. Pattern matching costuma ser mais interessante para acessar o valor e você abstrai outros comportamentos que são mais interessantes para serem encapsulados como “operações monádicas” (a documentação da abstração Option em Rust é um ótimo exemplo).

Result

Se Maybe é a abstração para valor-ou-nada, Result é a abstração para valor-ou-erro. Suponha uma função que converta uma string para um inteiro de 32 bits. Erros podem acontecer e pode ser que você queira usar o Maybe<int> como retorno da função, mas talvez seja interessante diferenciar o porquê da função ter falhado, se foi devido a uma string inválida ou overflow, por exemplo.

Result como monad é bem interessante, porque lhe permite encadear uma cadeia de ações e, caso alguma delas retorne erro, a cadeia para, e o erro é armazenado, sem que você precise explicitamente verificar por sua existência. Na linguagem Rust, até existe uma macro, try!, que torna o código até mais legível a agradável.

É interessante ter operações monádicas em Result que só atuam em um dos valores (o esperado ou o valor de erro), então você teria Result<T, E>::map e Result<T, E>::map_err.

Future

Futures e promises se referem a um padrão na computação para abordar o problema de concorrência e dependência de dados. Nesse padrão, o future é um valor que embrulha o resultado, que é computado assincronamente. Alguma função como get pode ser usada para obter o resultado e, caso não esteja pronto, a thread atual é bloqueada até que ele esteja disponível.

A adição de operações monádicas permite definir uma API que anexe continuações que seriam executadas quando o resultado estiver pronto, livrando-lhe do problema de bloquear a thread atual (que essencialmente mata o paralelismo). C++ é uma linguagem que atualmente possui abstrações de futures e promises, mas não possui as operações monádicas que tornariam essa abstração muito mais agradável de ser utilizada, como é explicado em um bom texto do Bartosz Milewski.

Programadores de NodeJS querem usar futures para se livrar de códigos profundamente aninhados (também conhecido como nesting hell). Futures são monads que podem livrar o programador da verificação explícita de erros para cada operação. Se bind for uma função-membro, o aninhamento infernal é, de fato, eliminado. O benefício é um ganho na legibilidade que pode ter grandes consequências como produtividade aumentada e diminuição de erros.

As três propriedades de um monad

A terceira propriedade existe para garantir ordem. E isso é importante para composibilidade.

Conclusão

Utilidade. A utilidade de você entender monads é que agora, quando os programadores de Haskell invadirem sua comunidade de programação, você vai entender o que eles querem dizer por monad. A utilidade se resume a comunicação, mas eu não recomendo que você use o termo monad para documentar abstrações que você crie que modelem o conceito de monad, pois é inútil. Dizer que uma abstração modela o conceito de monad não vai me fazer querer usar tal abstração. Demonstrar a utilidade da abstração vai me fazer querer usá-la e omitir o termo monad não vai tornar a demonstração mais longa ou menos clara. Só use o termo monad para questão de comunicação, quando você estiver tentando se comunicar com programadores de Haskell.

A fantástica Rust e o meu novo emprego fantástico

Minha camisa do orgulho Rust

Esse não é o primeiro post no meu blog onde menciono Rust, mas a partir de agora, as postagens sobre essa linguagem fantástica devem se tornar mais frequentes. Rust é uma linguagem de programação de sistemas e o que eu tenho para dizer, em uma linha, sobre Rust, é que eu acredito em Rust.

Muitas linguagens para diminuir erros e aumentar a produtividade foram criadas, mas elas costumam ser mais lentas. E para se sobressair e causar hype, uma abordagem de marketing recente foi criar novas linguagens afirmando que elas são linguagens de programação de sistema, apesar de serem tão produtivas quanto as linguagens mais lentas. Essas ditas linguagens de programação de sistema costumam (1) depender de um Garbage Collector e (2) produzir binários gigantes. O problema #1 as torna uma não-opção para mim e eu deixei minha crítica registrada em um texto separado. O problema #2 as impede de serem utilizadas em projetos embarcados, onde costumamos utilizar linguagens de programação de sistemas de verdade, como C.

Enfim, Rust é a linguagem de programação de sistemas que, não só afirma ser uma linguagem de programação de sistemas, como também, de fato, é uma linguagem de programação de sistemas. E ela tenta aumentar sua produtividade de duas formas:

  • Fornecendo construções comuns em linguagens de alto nível.
  • Aumentando a segurança, diminuindo o tempo que você iria passar depurando problemas.

Entretanto, para aumentar a segurança, Rust move muitos erros, que antes aconteciam em tempo de execução, para tempo de compilação. E para escrever código Rust que compile, você precisa aprender alguns conceitos que não são tão comuns. Logo, os erros diminuem, mas o tempo que você passa batalhando contra o compilador aumenta. Em outras linguagens, você ia gastar esse tempo escrevendo testes. Escrever testes ainda é legal, mas você não vai escrever testes para garantir segurança de memória e coerência de threads, porque o compilador garante isso para você. E, como o compilador garante isso para você, você vai gastar muito menos tempo ponderando se seu código faz sentido ou não, o que também aumenta a produtividade. Um depoimento que eu gosto, é o seguinte, feito por Mitchell Nordine:

I’ve found that Rust has forced me to learn many of the things that I was slowly learning as “good practise” in C/C++ before I could even compile my code. Rather than learning by trying to solve segfaults, debugging my pointer messes, etc, Rust tells me at compile time why what I’m trying to do is probably not a wise choice.

Se você simplesmente não se importa em produzir código correto, então outras linguagens ainda devem oferecer uma produtividade melhor. Claro que outra complicação na curva de aprendizado de Rust vem do fato que ela também não quer abrir mão de performance.

Eu não consigo levar a sério, linguagens que não levam performance a sério. E eu não consigo considerar o uso de linguagens que dão significado a espaço em branco (ex.: blocos definidos por identação, como em Python, mas tem até coisa pior em Haskell) ou ao estilo de capitalização dos nomes (i.e. se começar com maiúscula, é uma classe). Enfim, pela primeira vez, apareceu uma linguagem que conseguiu me fazer deixar C++ de lado. E Rust acerta muitas vezes, então a propaganda feita varia de acordo com a combinação anunciante/ouvinte.

Emprego novo

GitHub Orgs

E, para combinar com a linguagem fantástica, tenho a declarar que eu consegui um emprego fantástico, no projeto MaidSafe (que é um projeto fantástico que merece seu próprio post). Estarei trabalhando de home office e ainda irei trabalhar com assuntos que me excitam: Rust, software livre e redes descentralizadas.

Eu amei a descrição do emprego feita por um twitteiro aleatório:

“surely the most dev hipster job out there right now. @maidsafe. programming in rust, blockchain distributed ID”

Uma nota rápida: tecnicamente, eu estou trabalhando para mim mesmo, oferecendo serviços de consultoria, e o projeto MaidSafe é meu cliente. Só esclarecendo, pois não posso fazer afirmações de que trabalho para o projeto MaidSafe no sentido clássico (e é basicamente assim com o resto dos funcionários).

Eu queria ir com mais calma e terminar mais projetos antes de assumir tamanha responsabilidade, mas minha vida é simplesmente muito maluca para que eu preveja seu futuro.

Para ser honesto, o pessoal da MaidSafe é bem amigável e compreensível (pelo menos até agora), e me deu bastante liberdade para ter horários flexíveis que vão me permitir terminar projetos paralelos e continuar trabalhando no meu fantástico estágio envolvendo bindings de JavaScript para as bibliotecas EFL (tem até um rascunho de um post sobre isso que estou postergando em terminar e publicar já faz muito tempo).

E alguém importante para que eu conseguisse esse emprego foi o Francisco Lopes, por ter me convencido a fazer a entrevista, e fazer logo, aumentando minha confiança. Logo, eu devo a ele um ou quatro copos de cerveja, que pretendo pagar em algum dos encontros da lista de C/C++ Brasil.

BEER!

No tópico de Garbage Collector

Você quer que sua linguagem tenha Garbage Collector. Isso é uma mentira.

Você quer que sua linguagem tenha gerenciamento automático de memória. Isso ainda é uma mentira, mas pelo menos é uma mentira menor.

Você quer que sua linguagem tenha gerenciamento automático de recursos, não importando se eles são memória, file descriptors, socket descriptors ou qualquer outro. Isso NÃO é mentira.

Gerenciamento de memória (e outros recursos) seguro, automático, barato, performático e sem Garbage Collector é possível, como a linguagem Rust vem provando. Rust é a linguagem formidável que realmente se preocupa com performance e é capaz até de detectar data race em tempo de compilação.

Problemas de usabilidade com Garbage Collector

Garbage Collectors não possuem semânticas fortes o suficiente para permitir destrutores. O mais próximo que existe são os finalizers, que possuem uma lista bem grande de possíveis problemas.

É justamente devido ao Garbage Collector e aos finalizers que você está tendo mais trabalho, tendo que gerenciar blocos finally após cada tratemento de exceção. Na verdade, eu também não acredito em tratamento de erros usando exceções (leia o link para não entender errado!).

Outros problemas com Garbage Collector

O uso de um Garbage Collector diminui a utilidade do seu programa. Se seu programa possuir requisitos de tempo real, baixa latência, sistemas embarcados, uso eficiente de bateria ou alguns outros relacionados, Garbage Collector se torna indesejável.

Uma crítica inicial a Garbage Collector é que seu programa congela aleatoriamente, prejudicando a responsividade, mas esse problema é até mais prejudicial no caso do programa ter requisitos de tempo real, pois nenhuma garantia de tempo máximo gasto com coleta de lixo é oferecida. Foram então, criados algoritmos de GC que rodam em paralelo e algoritmos incrementais de GC, permitindo o uso de GC nesses cenários, mas outros problemas persistem. Primeiro, que uma solução dessas continua a consumir mais recursos.

Outra crítica a GC é que ele é um consumidor exagerado de recursos. Se o algoritmo é paralelo, você já depende de uma segunda thread. A depender do algoritmo adotado, você já vai precisar do dobro de memória RAM que uma versão do programa sem GC. E o programa estará usando mais memória que o que precisa na maior parte do tempo, então não podemos chamá-lo de “leve”. Tudo isso contribuindo para o mal uso da bateria do dispositivo.

Mais uma crítica ao GC é que, quando ele executa, vai caminhar aleatoriamente na RAM, navegando de ponteiro em ponteiro, poluindo a cache da CPU e impedindo que você crie um programa de baixa latência, onde responsividade é importante (i.e. jogos).

Um tweeteiro aleatório me deixou ciente de um diagrama legal que pode facilmente lhe fazer pensar que talvez “C++ sempre vença no desafio de performance”. Acho tal gráfico relevante para o tema:

Conclusão

Eu não tenho nada para concluir de verdade. Só criei essa seção para ter a oportunidade de usar uma frase: C++ foi minha escola.

EDIT:

É legal ver a cara de “WTF” do Alexandrescu ao escutar o Stephan T. Lavavej falar “I don’t think garbage collection can ever solve it”.

Tratamento de erros usando exceções sob a perspectiva de um programador de C++

Um tópico sobre o qual sua compreensão pode evoluir muito com o tempo, pois pouca experiência tem uma grande contribuição para impedir que os conceitos se tornem claros é o tópico de tratamento de erros usando exceções. Em C++, esse é um assunto até mais polêmico que em muitas outras linguagens e eu resolvi compartilhar um pouco de minha compreensão nesse texto.

Eu quero que você se esforce muito para não pensar nas implicações de performance que exceções introduzem no sistema, pelo menos até terminarmos de analisar as outras facetas dessa ideia, pois há muitos argumentos irracionais, que, na minha experiência, vêm principalmente de pessoas que estão falsamente preocupadas com performance. Se esforce para manter em mente as frases “um erro é uma exceção?” e “o que é um caso excepcional?”. Elas vão ajudar a entender motivações e serão discutidas mais adiante no texto.

Exceções – resumo/revisão rápida

Exceções formam um conceito de programação idealizado para tornar difícil o ato de ignorar erros, separar código do fluxo normal do programa do código de tratamento de erros — aumentando a legibilidade, diminuindo erros e melhorando a manutenção do sistema — e transportar tanta informação sobre o erro quanto possível.

A motivação para exceções terem sido idealizadas é inspiradora, principalmente quando se pensa em estilos de notificações de erros alternativos que eram/são comuns, como variáveis globais (errno), valores de retorno onde alguns valores possíveis são reservados para indicar erro (normalmente -1), valores de retorno representando um código de erro, valores de retorno indicando sucesso ou erro e outros. Algumas dessas técnicas alternativas poluem abstrações, como no caso onde o valor de retorno da função torna-se reservado para o erro e a função precisa ser refatorada para enviar o resultado através dos argumentos passados para ela.

Algumas das técnicas alternativas evoluíram para soluções que chegaram a adquirir o meu selo de “minimamente respeitável”, e agora nós temos outras soluções para se levar a sério, como, por exemplo, verificações forçadas estaticamente através do sistema de tipos (veja o exemplo de Rust). Mas, esse texto é sobre exceções, então, a partir de agora, evitarei menções desnecessárias a essas abordagens alternativas.

Uma proposta de “expected” para C++ sugere quatro características importantes para um sistema de tratamento de erros:

  • Visibilidade do erro.
  • Informação nos erros.
  • Código limpo.
  • Erros não intrusivos.

Eu recomendo a leitura da parte inicial da proposta para entender essas quatro características. Elas ajudam a julgar o que é um bom sistema de tratamento de erros, ressaltando indicativos importantes.

Isso é introdução mais que o suficiente para se começar a descrever o que são e como funcionam exceções. Exceções definem construções para o controle de fluxo de execução do programa onde separamos código “normal” de código “excepcional”. E existe a exceção, usada para representar e transportar informações sobre um erro. Quando um erro é detectado e a exceção correspondente é gerada, o fluxo de execução normal é abortado e o fluxo de execução excepcional é iniciado, recebendo a exceção lançada para que se possa fazer a decisão de tratamento de erro apropriada.

Sintaticamente, envolvemos o fluxo de código normal para o qual somos capazes de tratar alguma exceção usando um bloco “try” e, logo em seguida, introduzimos um bloco “catch”, que contém o código para o fluxo excepcional. Um exemplo é dado abaixo:

Bem simples e, para muitos novatos, epifanicamente elegante.

Onde exceções decepcionam

O principal problema de usabilidade com exceções, tal como é implementado na linguagem C++, é que você nunca está certo de que está tratando todas as exceções possíveis. Você chama uma função, verifica a assinatura dela, e não sabe quais possíveis exceções serão lançadas. E se, após uma atualização, a função passar a lançar um novo tipo de exceção, todo o código que a chama vai continuar compilando normalmente, sem nem mesmo um aviso sobre a necessidade de atualizar o código chamador, mas causará um erro em tempo de execução nos piores momentos — que são os momentos quando o programador está confiante que o sistema está pronto para produção.

Em Java, há uma funcionalidade chamada de exceções verificadas, em que as possíveis exceções a serem lançadas por uma função fazem parte de sua assinatura. Ou seja, o sistema de exceções é integrado ao sistema de tipos para aumentar a segurança do sistema. Com tal funcionalidade, se, para uma dada função, não há a declaração de que uma exceção X é possivelmente lançada, e essa possibilidade existe no corpo da função, um erro de compilação é gerado. Ou você trata todas as exceções que podem ser lançadas dentro da própria função, ou informa ao código chamador que é possível que algumas exceções sejam lançadas.

Há quem considere catch(…) com rethrow um devido tratamento de exceção, mas a exceção não está sendo tratada. Você está meramente ignorando toda a informação sobre o erro e transferindo a responsabilidade de tratá-la para algum outro lugar. Esse padrão é útil, no entanto, para fornecer garantias de exceções básicas e fortes, dois conceitos presentes na STL.

Não é possível deferir o tratamento de exceções, situação que já podemos encontrar em casos apenas um pouco mais distantes que os imaginados e idealizados. Em programação concorrente (assíncrona ou paralela), o ponto onde uma operação é iniciada e onde é completada podem ser diferentes. Tais pontos sendo diferentes, a exceção não é retornada para o código que requisitou a realização da operação, e você simplesmente usa outro paradigma para tratamento de erros. É até mais complicado quando se envolve threads — problema para o qual o padrão C++11 adicionou exception_ptr. Um dos paradigmas, o paradigma de futures & promises, restaura o suporte a exceções nesses casos, fazendo com que a operação de desembrulhar o valor, através da função-mebro get, possivelmente lance uma exceção, mas, para funcionar, esse truque se apoia no fato de que as exceções em C++ não são verificadas.

Um ponto final sobre a complexidade que exceções introduzem está relacionada a código de finalização, que sempre precisa ser chamado, independente de uma exceção ter sido lançada ou não. Se a linguagem não suporta RAII, adiciona blocos “finally” para resolver o problema. Se a linguagem suporta RAII, um conjunto de convenções e regras é criado e a possibilidade de se lançar uma exceção enquanto outra já está sendo lançada nasce, o que normalmente provoca o programa a ser terminado.

Basicamente, exceções podem se tornar o novo goto e, elas são até abusadas, para, por exemplo, se implementar sistema de pattern matching em C++.

Performance

Finalmente, o tópico que causa polêmica e que desencoraja o uso de exceções em qualquer caso que não seja excepcional, mas cuja preocupação ajuda a definir o que é um caso excepcional.

A primeira observação a se fazer sobre exceções é que elas não são lentas. Citando outro texto de minha própria autoria:

O documento TR18015 detalha duas abordagens principais para a implementação de suporte a exceções e o que o documento menciona como “abordagem tabela” é muito barata, realizando boa parte do que precisa ser computado em tempo de compilação e estruturando o código de forma que não há penalidade em tempo de execução para o fluxo de execução normal do programa. O custo, nessa abordagem, é que o programa fica maior, mas a parte extra pode ser movida para não interferir negativamente no uso de cache e, em sistemas com suporte a memória virtual, essa parte pode até ficar na swap até que torne-se necessária.

uma resposta no StackOverflow, caso você não queira se dar ao trabalho de passar pelo TR18015. A implementação do suporte a exceções é bem elegante e a discussão deveria deixar de ser se exceções são lentas e passar a ser quando usar exceções.

E cuidado com os benchmarks injustos que alimentem esse mito, assim como o P. alerta.

Então usar exceções pode conduzir a um código até mais rápido do que manualmente verificar se erros ocorreram, mas caso um erro aconteça, haverá uma drástica redução na performance. E, como exceções dependem do mecanismo de RTTI, pode ser que tratar erros usando exceções deixe de ser uma opção em sistemas de tempo real.

A solução para esse problema está em colocar uma linha divisória entre erros e exceções. Ou, mais objetivamente, o que é um caso excepcional. Se você usar exceções apenas em casos excepcionais, não há problema nenhum em utilizá-las devido a alguma degradação de performance, pois você atingiu um caso excepcional de qualquer forma.

Um exemplo de um caso excepcional é quando o seu sistema encontra um erro de lógica, pois continuar executando o programa pode causar corrupção do seu estado interno e ele deveria ser encerrado o mais cedo possível, parando somente para mostrar uma mensagem de erro ao usuário.

Outro exemplo de caso excepcional é um cliente de um jogo online, onde, caso a conexão caia, você não se importa com o tempo gasto para lançar a exceção, pois o sistema não pode fazer nada de útil enquanto o erro não for tratado, e reconectar ao servidor é uma operação que vai custar mais tempo que o tratamento da exceção, de qualquer forma. Esse mesmo exemplo de “queda de conexão”, entretanto, pode virar um caso não excepcional em um contexto diferente. No contexto de um servidor que está servindo milhares de clientes, por exemplo, quedas de conexões são esperadas o tempo todo e você não quer que o gargalo de lançar milhares de exceções diminua a capacidade do seu servidor.

O usuário do reddit @sazzer ilustra mais alguns casos do que é uma situação excepcional.

Conclusão

Responder a pergunta “esse é um caso excepcional?” é uma tarefa complicada, mas, pode se tornar mais fácil, fazendo uso de bom senso, das ferramentas certas e de um bom entendimento de suas implicações.

Além disso, um mesmo erro pode ser excepcional ou não a depender do contexto, então é preferível que se evite a dependência em exceções.

Aliado a todos os problemas que exceções trazem, eu sugiro que seu uso, em C++, seja reduzido. Entretanto, você simplesmente não pode se livrar de exceções usando C++ idiomático, então compreendê-las é importante.

Um sistema de tratamento de erros unificado é atraente, pois elimina a chance de escolhermos a abordagem errada em algum ponto. Entretanto, um sistema de tratamento de erros unificado deveria ter todas as vantagens que exceções possuem e se livrar de seus problemas mais sérios, inclusive os problemas de performance que impossibilita seu uso em sistemas de tempo real.

Pessoalmente, o sistema de tratamento de erros que eu vejo como ideal é a abordagem que é adotada em Rust, que emprega uso de conceitos como algebraic data types e pattern matching para oferecer um sistema de tratamento de erros bem expressivo e que é difícil de usar erroneamente.

UPDATE:

Acabei encontrando um texto interessante sobre a luta de um usuário para diminuir o tamanho da imagem do programa quando exceções são habilitadas.

%d blogueiros gostam disto: