.NET

13 fev, 2008

Dominando C++

Publicidade

Discutiremos neste artigo os princípios de projeto mais importantes que nortearam o desenvolvimento da linguagem de programação C++.

Pré-requisitos:

Bons conhecimentos em programação estruturada; conceitos básicos de Programação Orientada à Objetos (POO);

Objetivos

Ao final desse artigo, espera-se que o leitor seja capaz de:

  • Compreender os propósitos da linguagem C++
  • Entender os passos necessários para aprender a linguagem de uma forma adequada
  • Identificar situações nas quais C++ oferece vantagens em relação a outras linguagens
  • Reconhecer a importância de planejar casos de teste, gerência de memória, e outras atividades antes de começar a implementação de qualquer aplicação em C++

Introdução

O presente artigo inicia uma jornada que pretendo seguir pela linguagem de programação C++. Diversos assuntos serão abordados, dentre os quais POO (o assunto abordado em alguns dos próximos artigos); os containers da biblioteca padrão; iterators; streams; smart pointers, templates e metaprogramação, além dos excelentes recursos da biblioteca boost[1].

Especificamente em relação aos próximos tópicos, pretendo escrever sobre os mecanismos de herança, herança múltipla; funções friend; métodos virtuais e polimorfismo; classes genéricas e sobrecarga de operadores.

Há, entretanto, algumas coisas que devem ser ditas sobre a linguagem antes de começarmos os assuntos técnicos previamente mencionados. Em primeiro lugar, o leitor deve ter ciência de que C++ é muito mais rica, poderosa e, consequentemente, mais complexa do que a maioria das pessoas imaginam. Logo, o seu aprendizado exige muito mais esforço, tempo e cuidado. Se uma pessoa que nunca programou em uma linguagem orientada a objetos se propor em aprender a sintaxe de C++ dentro três semanas, isso será perfeitamente possível – a depender da dedicação dispensada. Com C++, no entanto, apenas a sintaxe não basta – ao menos dentro do seu alto nível de exigência. Com essa poderosa linguagem, qualquer deslize pode ser fatal. Os pequenos detalhes podem dar muita dor de cabeça e se tornarem uma pedra no sapato até mesmo dos mais experientes programadores.

Portanto, aprender a programar em C++ é como aprender a dançar ou a executar qualquer atividade que exija a repetição de movimentos: se você adquire qualquer vício e acostuma-se, o resultado pode ser desastroso. Só para que você sinta o drama, há certos movimentos no jogo de tênis que, se executados repetidamente de forma incorreta, quase sempre levam a sérias lesões. O mesmo acontece aqui.

A propósito, antes que você pergunte, sim, C++ é uma linguagem bastante intolerante com programadores novatos. Porém, como veremos, isso não significa que ela seja indomável.

1. Um pouco de história

Nosso objetivo aqui não é fornecer a história completa do desenvolvimento de C++, mas prover uma breve contextualização ao leitor em relação à época e as pessoas envolvidas no desenvolvimento da linguagem.

Para tanto, basta que o leitor saiba que as primeiras idéias para criar uma linguagem como C++ foram inicialmente concebidas no fim da década de 70 durante os trabalhos de doutoramento de um estudante dinamarquês, Bjarne Stroustrup, em Cambridge, Inglaterra. Ele estava buscando formas alternativas de organizar a construção de softwares para sistemas distribuídos. Conforme explica em seu livro sobre a história da linguagem [2], os detalhes desse trabalho de doutorado não foram relevantes para C++. Entretanto, ao construír um simulador escrito na linguagem orientada a objetos Simula, Stroustrup começou a perceber a necessidade de uma linguagem que oferecesse os mesmos recursos de Simula, porém mais estável e eficiente. Assim surgiu C++ a qual, em uma primeira versão, foi apelidada de “C com classes”.

Portanto, a principal influência de C++, (no que tange à orientação a objetos) ao contrário do que algumas pessoas imaginam (ver nota 1 ao fim do artigo), é mesmo Simula. Na tabela 1, um resumo cronológico dos principais acontecimentos no desenvolvimento e popularização da linguagem.

Tabela 1 - Cronologia dos principais eventos no desenvolvimento de C++.Tabela 1 Cronologia dos principais eventos no desenvolvimento de C++.

2. Filosofia da Linguagem

O arquiteto da linguagem, Bjarne Stroustrup, ainda no livro sobre a história da linguagem[1], esclarece alguns pensamentos que o levaram a projetar C++ da forma como hoje se encontra. A seguir, um resumo das idéias por detrás do projeto da linguagem:

  • C++ é projetada para ser uma linguagem estaticamente tipada e de propósito geral tão eficiente e portável quanto C
  • C++ é projetada para fornecer um suporte direto e compreensível a múltiplos estilos de programação (programação procedural, abstração de dados, POO e programação genérica)
  • C++ é projetada para permitir que o programador faça as escolhas, mesmo que isso possibilite que o programador faça a escolha errada
  • C++ é projetada para ter o máximo de compatibilidade com C, provendo, assim, uma transição suave a partir de C
  • C++ evita utilizar recursos que são dependentes de plataforma ou que não sejam de propósito geral
  • C++ não adiciona overhead para recursos que não são utilizados
  • C++ é projetado para funcionar mesmo na ausência de ambientes de programação sofisticados

Da lista acima, destaquei os três princípios de projeto que qualquer pessoa que se disponha a aprender C++ deve ter ciência. A partir desse ponto, o artigo dedica-se a interpretar e a elucidar esses princípios.

2.1 A Marcha do Progresso

“A simplicidade consiste em extrair o óbvio e acrescentar o significativo” (John Maeda)

Primeiro, percebe-se que o projetista da linguagem escolheu cautelosamente o suporte simultâneo a diferentes paradigmas de programação. Por quê? Se você programa em Java ou outra linguagem Orientada a Objetos, deve estar se perguntando:

De que forma misturar POO com programação procedural pode trazer alguma vantagem para o desenvolvedor?

É aí onde mora o perigo. É justamente nesse ponto onde muitos desenvolvedores gastam (perdem) preciosas fatias de tempo em duradouras e calorosas discussões. O fato é que não há uma resposta conclusiva. Tudo irá depender do contexto e das características do projeto. Mas, só para não deixar o leitor curioso, os desenvolvedores C++ possivelmente teriam a palavra simplicidade na ponta da língua:

Por que perder tempo projetando um conjunto de classes quando você poderia resolver esse simples problema com um simples conjunto de funções utilitárias agregadas em um mesmo namespace?

Não consegue ainda visualizar a discussão? Então nada como o excelente exemplo da evolução do poder de expressão das linguagens para o problema de exibir uma saída numérica formatada, extraída do site pessoal do professor de Ciência da Computação, programador e autor de diversos livros sobre C++ e Java, Cay Horstmann[3]:

C em 1980:


printf("%10.2f", x);

C++ em 1988:


cout << setw(10) << setprecision(2) << showpoint << x;

Java em 1996:


java.text.NumberFormat formatter = java.text.NumberFormat.getNumberInstance();
formatter.setMinimumFractionDigits(2);
formatter.setMaximumFractionDigits(2);
String s = formatter.format(x);
for (int i = s.length(); i < 10; i++)
    System.out.print();
System.out.print(s);

Java em 2004:


System.out.printf("%10.2f", x);

Lembre-se: em geral, soluções orientadas a objeto demandam muito mais esforço a curto prazo do que programação estruturada. No entanto, a longo prazo, a POO pode se beneficiar de um bom projeto onde fatores como encapsulamento, extensibilidade e reusabilidade foram previamente concebidos. Assim, se o seu problema é simples de se resolver e você acredita que ninguém mais no universo realmente precisaria remodelar o seu pequeno conjunto de código utilitário, então…

“[Flexibilidade é] a facilidade com a qual um sistema ou componente pode ser modificado para uso em aplicações ou ambientes diferentes daqueles para os quais ele foi especificamente projetado” (IEEE Standard Computer Dictionary)

Bom, eu não quero me ater muito em discussões do tipo “POO vsprocedural”. Nem tampouco quis Stroustrup o mesmo, ao projetar a linguagem. Talvez seja essa a principal justificativa para se agregar aqui vários paradigmas: flexibilidade. Essa característica é o que torna a linguagem expressiva o suficiente para adequar-se a vários modelos formais de especificação de sistemas. Portanto, tenha isso em mente ao iniciar os seus estudos em C++: não importa o quanto você se julga autosuficiente em outras linguagens e paradigmas, há muito ainda a ser descoberto e você certamente precisará de muita dedicação no início.

2.2 Você pode, mas vá com calma!

“Escreva o que você conhece e conheça o que você escreve” (Herb Sutter)

Passemos ao segundo item em destaque na lista, “ter o poder de decisão”. Esse princípio não se aplica somente ao(s) paradigma(s) escolhido(s) para o desenvolvimento, mas também aos mínimos detalhes de implementação. Sendo muito franco, para se desenvolver qualquer software estável e comercial em C++ é necessário ter uma plena consciência do universo de possibilidades oferecidas pela linguagem.

Estou começando a ficar preocupado. Toda essa flexibilidade é realmente necessária? – você, estimado leitor, sabiamente me questiona.

Vamos então por partes. Primeiro, se você está ficando “preocupado”, excelente! Finalmente você capturou a essência da questão, mas ainda falta um pouco mais. Um dos pré-requisitos básicos para se tornar um bom programador C++ é ser “paranóico”. Afinal (não sei se a essa altura você já deduziu isso), ter o poder de decisão em suas mãos implica que o programa codificado não irá simplesmente sair fazendo coisas como gerenciar memória só porque alguém no mundo achou que seria legal que outras linguagens pudessem especificar programas que fazem isso automaticamente.

Percebeu? Um bom projeto de gerência de memória é essencial antes que um programador inexperiente saia por aí com sua possante máquina a fazer o que bem quiser. A propósito, C++ não suporta coleta de lixo (garbage collection) automática, embora você possa utilizar bibliotecas de terceiros que fornecem excelentes implementações de coleta de lixo, se você julgar realmente necessário.

Nesse ponto, é exigido do programador tanto o conhecimento das áreas de memória relativas a um programa em C++ quanto em relação ao próprio sistema operacional. Como recomenda Herb Sutter[4], você precisa compreender as diferentes áreas de memória em termos de como elas diferem e como se comportam. Certamente isso demanda tempo, especialmente se você vem de uma linguagem que abstrai esse aspecto em demasia. Assim, chegamos à conclusão de que ter uma linguagem como C++ em mãos requer um bom conhecimento de aspectos que vão muito além de saber a sintaxe e a semântica da linguagem. E você precisará desse conhecimento para evoluir em C++.

Quanto à necessidade de ser flexível, a resposta é sim. C++ talvez seja uma das linguagens onde, por conta e risco do programador, a doce sensação de segurança da linguagem pode ser abandonada. Em nome de quê? Geralmente, performance. Por exemplo, considere a última linha do seguinte trecho de código em Java:


Collection meusAutores = new ArrayList();
meusClientes.add( new Autor("Bjarn Straustrup") );
Pessoa p = (Pessoa) meusAutores.get(0);

Na linha 3, pode-se até ouvir a JVM (Máquina Virtual Java) sussurrando:

Eu sei exatamente o que você quer que eu faça nessa linha.

E é exatamente disso que estamos falando. A JVM infere muito mais coisas sobre a linha 3 do que aquilo que, semanticamente, a linguagem Java expressa, de modo que o programador pode simplesmente ignorar certos comportamentos (quer conscientemente ou não). A chamada de método get(0) não apenas retorna uma cópia do valor desejado (no caso, a referência para o objeto anônimo criado na Linha 2). Ela acessa várias informações para decidir se você está ou não estourando os limites do array meusAutores e se uma “exceção de cast em tempo de execução” deve ou não ser levantada, para, só depois, retornar a referência que você tanto espera (a despeito da perda de informações fruto da conversão de um tipo maior para um menor).

Isso não significa que essas verificações de Java sejam algo negativo e nem tampouco que C++ não possua seus prórios mecanismos de tratamento de erros. A propósito, C++ usa exceções de forma similar a abordagem de Java, além de possuir funções de acesso padrão que realizam a verificação de índices numéricos para estruturas de dados seqüênciais. Mas há uma diferença, como veremos no exemplo a seguir.

Suponha agora que você deseje fazer exatamente a mesma coisa de forma muito mais eficiente. Suponha ainda que o seu programa, ao invés de manter uma lista de livros, seja uma aplicação gráfica onde você deseje manter uma lista com milhares de pixels que precisam ser processados instantaneamente para realizar a aplicação de um novo filtro desenvolvido por sua companhia de software à imagens de alta resolução. Agora as coisas se complicam. Terá o seu prezado usuário a paciência necessária para aguardar o tempo em que for preciso para que o programa realize todas as verificações que julgar necessárias? Será que você, em nome da eficiência (e tendo se assegurado sobre a corretude do seu código), não poderia simplesmente dizer à JVM para desabilitar essas exceções? Eu sinceramente não sei se há algum modo de se conseguir isso, mas a resposta é provavelmente não.

Ou seja, em alguns casos, ter o poder de escolha é um atributo altamente desejado em uma linguagem de programação, desde que essas escolhas sejam realizadas com muito cuidado e consciência. Nesse caso específico, C++ é indiferente ao fato de o programador ter assegurado ou não que o código acessa somente posições válidas em um array. Além disso, a critério do programador, não verifica a eventual incompatibilidade de uma conversão de tipo. Melhor (ou pior) ainda, se o programador optar por uma estrutura nativa da linguagem (no caso, um array), é possível usufruir da aritimética de ponteiros para se ter um acesso ainda mais eficiente.

2.3 Zero overhead

Chegamos, assim, ao último item destacado na lista de princípios de projeto levantados por Bjarne Straustrup, como uma conseqüência direta dos dois itens explanados anteiormente: zero overhead. Por qual motivo adicionar processamento extra para recursos que não são necessários?

3. Aprendendo a aprender

“Se uma mudança dramática na forma com que trabalhamos e com que pensamos em construir sistemas não é o nosso foco, então por que se preocupar em aprender uma nova linguagem?” (Bjarne Stroustrup)

Se você teve a paciência de ler esse artigo até aqui, esse é o momento em que eu lhe suplicarei para não interpretá-lo errôneamente. Não estou dizendo que C++ é a oitava maravilha do mundo. Os mesmos princípios de projeto aqui expostos e discutidos são motivo de severas críticas feitas à linguagem. Aliás, foi devido à insatisfação de um proeminente desenvolvedor C++, James Gosling, em relação aos recursos considerados “perigosos” da linguagem (tal como ponteiros) que a idéia de se construir Java foi amadurecida. Se não utilizados corretamente, os recursos “sombrios” de C++ podem resultar em grandes perdas em tempo de debugging.

Portanto, se, circustancialmente, produtividade é uma palavra mais em alta do que performance em sua equipe de projeto, provavelmente você não terá maiores motivos para querer aprender C++ a curto prazo. Ainda assim, há excelentes razões para você querer aprender C++ a médio/longo prazo, mesmo que nenhuma empresa de software que você conheça esteja desenvolvendo projetos nessa linguagem. E a principal razão é a de que você irá expandir muito a sua visão, explorando aspectos que outros programadores não costuma se preocupar com freqüência. E tendo-se adquirido um bom nível em C++, não haverá mais empecilhos que não o permitam obter, juntos, produtividade e performance no desenvolvimento de um sistema.

Mas antes de encarar os estudos, há ainda algumas coisas que você deve saber.

3.1 Não será fácil…

Como conseqüência da abordagem adotada pelos engenheiros da linguagem, a curva de aprendizado de C++ é muito elevada. Talvez, se você possuir experiência prévia em C, isso poderá lhe ajudar um pouco. Talvez você esteja pensando nesse momento:

Não estou muito impressionado. Eu já programei em C e li capítulos da obra de Kernighan e Richie (ver nota 2 ao fim do artigo) . Agora, eu estudo C++ por conta própria: visitei alguns fóruns russos e americanos, e hoje já tenho domínio suficiente para construir classes e utilizar algumas estruturas da biblioteca padrão.

Se esse realmente esse for o seu caso, tenha cautela. O problema (talvez você tenha mais sorte que eu) é que todo esse conhecimento pode não ser suficiente para fazê-lo alcançar o objetivo primordial de qualquer programador que é o de escrever programas corretos e que façam uso racional dos recursos. É notadamente adimitido pela comunidade de desenvolvedores C++ que evitar memory leak (vazamentos de memória) é uma tarefa árdua em programas de médio porte. E, talvez, você corra o risco de ter adquirido alguns vícios (ex.: malloc e free de C) que possam comprometer a sua capacidade de escrever programas de forma eficiente.

Tenha em mente que, como afirma Sutter[4], não há nada de errado em enxergar C++ no início como um “C melhorado”, contanto que você tenha a consciência de que há muito mais recursos e conceitos a serem explorados em C++ do que C poderia sequer pensar em oferecer.

Quanto ao tempo de aprendizado? Aqui prefiro traduzir as palavras de Stroustrup no FAQ de seu próprio site[5]. Ele afirma que depende do que você endende sobre a palavra “aprendizado”.

“Se você programa em Pascal, você pode aprender facilmente os tipos básicos, estruturas de controle e funções de biblioteca de C++ para ser tão efetivo quanto você é usando Pascal. Se levará um dia ou uma semana? Depende da sua abordagem de estudo.” (Bjarne Stroustrup)

No entanto, ele explica que se você deseja estar completamente confortável com todas as princiapais construções da linguagem, abstração de dados, POO, programação genérica e projeto Orientado a Objetos, você poderá levar de um a dois anos, supondo que você ainda não domine completamente nenhuma dessas técnicas.

3.2 … mas valerá o esforço

Felizmente, em 1994 C++ passou por um processo de padronização através de um comitê da ISO que, tendo conluído as tarefas em 1998, enriqueceu a linguagem com uma fantástica biblioteca padrão a qual contém estruturas de dados e algoritmos extremamente eficientes. Além disso, os primeiros passos rumo à grandes melhorias no gerenciamento de memória foram dados com a introdução dos smart pointers. Uma vez que você domine os recursos oferecidos pela biblioteca padrão, você começará a sentir o verdadeiro poder da lingaugem.

Com o novo padrão previsto para 2009, C++ terá um poder de fogo ainda maior com a introdução de novas bibliotecas sobre expressões regulares, threads, gerência de memória, dentre outros.

Conlcusões

Apresentamos a linguagem de programação C++: sua história, seus princípios e suas características. Também foram discutidas as razões pelas quais você precisaria de C++ na construção de sistemas que possuam certos requisitos. Por fim, apresentamos um guia de como o estudo da linguagem deveria ser encarado por quem se propõe a aprender a programar em C++.

Mas espere. Qual seria, então, a maneira mais adequada de se construir uma base sólida de conhecimento em C++?

Resposta: compreendendo os problemas advindos da má utilização da linguagem e focando no aprendizado dos conceitos teóricos os quais lhe fornecerão uma base incontestável para lidar com essa fascinante linguagem de programação. Ainda, uma boa prática no aprendizado de C++ é a de codificar a solução dirigida por casos de teste, como recomenta Horstmann[6]. Nada melhor do que especificar todas as possíveis ocorrência de falhas antes de começar a codificar. Além de forçar-lhe a pensar em termos de algoritmo, essa prática lhe auxiliará a amadurecer a solução e a evitar desde cedo alguns erros comuns.

Felizmente existem maneiras práticas, seguras e eficientes de programar em C++ as quais iremos explorar nos próximos artigos.

Mais uma vez, agradeço a paciência de todos e desejo muito sucesso nos estudos.

Referências

[1] Bibliotecas C++ boost. Disponível em http://www.boost.org/

[2] Stroustrup, Bjarne (1994). The Design and Evolution of C++. Addison-Wesley.

[3] Site pessoal do professor Cay Horstmann. Disponível em http://www.horstmann.com/

[4] Sutter, Herb (2001). More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions. Addison-Wesley

[5] Site pessoal de Bjarne Stroustrup. Disponível em http://www.research.att.com/~bs/

[6] Horstmann, Cay (2005) Conceitos de Computação com o Essencial de C++. Bookman

Notas

1 Algumas pessoas pensam que C++ foi inspirado em Smalltalk. Mas o próprio Stroustrup desmente essa afirmação e classifica C++ e Smalltalk como “primas”, já que surgiram na mesma época e tiveram uma fonte comum de inspiração.

2 Autores de um livro clássico sobre C. Dennis Richie é o criador da linguagem C.