Grandes projetos de software geralmente são conduzidos por equipes de desenvolvedores igualmente grandes. Para que o código produzido por grandes equipes tenha qualidade mensurável na amplitude do projeto, o código deve ser escrito de acordo com um padrão; e ser julgado com ele. Portanto, é importante que as equipes de grandes projetos estabeleçam um padrão de programação ou conjunto de diretrizes.
O uso de um padrão de programação também torna possível fazer o seguinte:
O objetivo desse texto é apresentar regras de programação C++, diretrizes e dicas (genericamente conhecidas como diretrizes) que podem ser utilizadas como base para um padrão. Isso é destinado aos engenheiros de software que trabalham em equipes de grandes projetos.
A versão atual está propositadamente focalizada na programação (embora, às vezes, seja difícil traçar a linha entre a programação e o design); as diretrizes de design serão incluídas em uma data posterior.
As diretrizes apresentadas abrangem os seguintes aspectos do desenvolvimento C++:
Elas devem ser coletadas a partir de uma grande base de conhecimento de indústria. (Consulte a bibliografia para as origens: autores e referências.) Elas são baseadas em:
A maioria é baseada em uma porção da primeira categoria e grandes doses da segunda e da terceira. Infelizmente, alguns também são baseados na última categoria; principalmente, porque a programação é uma atividade altamente subjetiva: não existindo nenhuma forma aceita "melhor" ou "correta" para codificar tudo.
O código fonte C++ limpo e inteligível é o principal objetivo da maioria das regras e diretrizes: o código fonte limpo inteligível sendo o fator de maior contribuição para a confiabilidade do software e capacidade de manutenção. O que entende-se por código compreensível e limpo pode ser capturado nos três seguintes princípios fundamentais [Kruchten, 94].
Surpresa Mínima - Ao longo de sua existência, o código fonte é lido com mais freqüência do que escrito, especialmente as especificações. Em condições ideais, o código deve ser lido como uma descrição de idioma inglês daquilo que está sendo feito, com o benefício adicional que ele executa. Os programas são mais escritos para as pessoas do que para os computadores. O código de leitura é um processo mental complexo que pode ser facilitado pela uniformidade, também conhecido nesse guia como o princípio da surpresa mínima. Um estilo uniforme em um projeto inteiro é uma razão principal para que uma equipe de desenvolvedores de software concorde com os padrões de programação e não deve ser percebido como algum tipo de punição ou como um obstáculo à criatividade e à produtividade.
Ponto Único de Manutenção - Sempre que possível, uma decisão de projeto deve ser expressada em apenas um ponto na fonte e a maioria de suas conseqüências deve ser derivada programaticamente a partir desse ponto. As violações desse princípio representam grande risco à manutenção e à confiança, bem como ao entendimento.
Ruído Mínimo - Por fim, como uma grande contribuição à legibilidade, o princípio do ruído mínimo é aplicado. Ou seja, é feito um esforço para evitar a confusão do código fonte com o "ruído" visual: barras, caixas e outros textos com conteúdo de poucas informações ou informações que não contribuam para o entendimento do propósito do software.
A intenção pretendida das orientações aqui expressas, não deve ser excessivamente restritiva; mas sim tentar fornecer orientação para o uso correto e seguro dos recursos de linguagem. A chave para o bom software reside em:
As diretrizes apresentadas aqui criam um pequeno número de premissas básicas:
As diretrizes não são de igual importância; elas são medidas utilizando a seguinte escala:
Uma diretriz identificada pelo símbolo acima é uma dica de que uma parte simples do aviso pode ser seguida ou ignorada com segurança.
Uma diretriz identificada pelo símbolo acima é uma recomendação geralmente baseada em fundamentos mais técnicos: encapsulamento, coesão, acoplamento, portabilidade ou reutilização podem ser afetados, bem como o desempenho em algumas implementações. As recomendações devem ser seguidas, a menos que haja uma boa justificativa para não segui-las.
Uma diretriz identificada pelo símbolo acima é um requisito ou restrição; uma violação conduziria definitivamente ao código inválido, não confiável ou não portátil. Os requisitos ou as restrições não podem ser violadas sem uma concessão
Quando você não puder localizar uma regra aplicável ou diretriz; quando uma regra não se aplicar claramente; ou quando tudo mais falhar: utilize o senso comum e verifique os princípios fundamentais. Essa regra substitui todas as outras. O senso comum é requerido mesmo quando existirem regras e diretrizes.
Esse capítulo fornece orientação sobre a estrutura do programa e o layout.
Os sistemas grandes geralmente são desenvolvidos como inúmeros subsistemas funcionais menores. Os subsistemas em si geralmente são construídos a partir de inúmeros módulos de código. No C++, um módulo normalmente contém a implementação para um único, ou em raras ocasiões, conjunto deabstrações intensamente relacionadas. No C++, uma abstração normalmente é implementada como uma classe. Uma classe possui dois componentes distintos: uma interface visível aos clientes de classe, fornecendo uma declaração ou especificação dos recursos de classe e responsabilidades; e uma implementação da especificação declarada (definição de classe).
Semelhante à classe, um módulo também possui uma interface e uma implementação: a interface do módulo contém as especificações para as abstrações de módulo contidas (declarações de classe); e a implementação do módulo contém a implementação real das abstrações (definições de classe).
Na construção do sistema; os subsistemas também podem ser organizados em grupos colaborativos ou camadas para minimizar e controlar suas dependências.
Uma especificação de módulo deve ser colocada em um arquivo separado de sua implementação-o arquivo de especificação é conhecido como cabeçalho. Uma implementação de módulo pode ser colocada em um ou mais arquivos de implementação.
Se uma implementação de módulo contiver funções seqüenciais extensivas, declarações comuns de implementação privada, código de teste ou código específico de plataforma, separe essas partes em seus próprios arquivos e nomeie cada arquivo após o conteúdo de sua parte.
Se os tamanhos do programa executável forem um problema, as funções raramente utilizadas também devem ser colocadas em seus próprios arquivos individuais.
Construa um nome de arquivo de parte da seguinte maneira:
File_Name::=
<Module_Name> [<Separator> <Part_Name>] '.' <File_Extension>
A seguir está um módulo de exemplo que particiona e nomeia o esquema:
inlines
separado
(consulte "Colocar as Definições da Função Seqüencial do Módulo em um Arquivo
Separado").Module.Test
. Declarar
o código de teste como uma classe amigável facilita o desenvolvimento independente
do módulo de seu código de teste; e permite que o código de teste seja omitido do
código do objeto do módulo final sem mudanças de origem. SymaNetwork.hh // Contém a declaração para uma // classe denominada "SymaNetwork". SymaNetwork.Inlines.cc // Subunidade de definições seqüenciais SymaNetwork.cc // Unidade de implementação principal do módulo SymaNetwork.Private.cc // Subunidade de implementação privada SymaNetwork.Test.cc // Subunidade do código de teste
Separar a especificação de um módulo de sua implementação facilita o desenvolvimento independente do código do usuário e do fornecedor.
Quebrar a implementação de um módulo em várias unidades de conversão fornece melhor suporte para a remoção do código do objeto, resultando em tamanhos de executáveis menores.
Utilizar um arquivo regular e previsível que nomeia e particiona a convenção permite que o conteúdo de um módulo e organização sejam entendida sem inspeção de seu conteúdo real.
Transmitir os nomes através do código para o nome do arquivo aumenta a capacidade de previsão e facilita a construção de ferramentas baseadas em arquivo sem precisar do nome complexo que mapeia [Ellemtel, 1993].
As extensões de nome de arquivo geralmente utilizadas são:
.h, .H, .hh, .hpp
e
.hxx
para os arquivos de cabeçalho;
e .c, .C, .cc, .cpp
e .cxx
para implementações. Selecione
um conjunto de extensões e utilize-as consistentemente.
SymaNetwork.hh // A extensão ".hh" utilizada para designar // um cabeçalho de módulo "SymaNetwork". SymaNetwork.cc // A extensão ".cc" utilizada para designar // uma implementação de módulo "SymaNetwork".
O papel de trabalho padrão do rascunho C++ também utiliza a extensão ".ns" para os cabeçalhos encapsulados por um espaço de nomes.
Apenas em raras ocasiões as várias classes devem ser colocadas juntas em um módulo e, em seguida, apenas se estiverem intensamente associadas (por exemplo, um contêiner e seu iterador). É aceitável colocar a classe principal de um módulo e suas classes de suporte dentro do mesmo arquivo de cabeçalho, se todas as classes sempre precisarem ser visíveis a um módulo do cliente.
Reduz a interface de um módulo e as dependências de outros sobre ela.
Com exceção dos membros privados da classe, as declarações privadas de implementação de um módulo (por exemplo, os tipos de implementação e classes de suporte) não devem aparecer na especificação do módulo. Essas declarações devem ser colocadas nos arquivos de implementação necessários, a menos que as declarações sejam necessárias por vários arquivos de implementação; nesse caso as declarações devem ser colocadas em um arquivo de cabeçalho particular secundário. O cabeçalho particular secundário deve, então, ser incluído por outros arquivos de implementação, conforme necessário.
Essa prática assegura que:
// Especificação do módulo foo, contida no arquivo "foo.hh" // class foo { .. declarations }; // Fim de "foo.hh" // Declarações privadas para o módulo foo, contidas no arquivo // "foo.private.hh" e utilizadas por todos os arquivos de implementação foo. ... private declarations // Fim de "foo.private.hh" // Implementação foo do módulo, contida em vários arquivos // "foo.x.cc" e "foo.y.cc" // Arquivo "foo.x.cc" // #include "foo.hh" // Include module's own header #include "foo.private.hh" // Include implementation // declarações requeridas. ... definitions // Fim de "foo.x.cc" // Arquivo "foo.y.cc" // #include "foo.hh" #include "foo.private.hh" ... definitions // End of "foo.y.cc"
#include
para obter acesso à especificação de um módulo Um módulo que utiliza outro módulo deve utilizar a diretiva #include
do pré-processador para adquirir visibilidade da especificação do módulo do fornecedor.
De forma correspondente, os módulos nunca devem redeclarar qualquer parte da
especificação do módulo de um fornecedor.
Ao incluir os arquivos, apenas utiliza a sintaxe #include <header>
para os cabeçalhos "standard"; utilize a sintaxe #include
"header" para o restante.
O uso da diretiva #include
também se aplica aos próprios arquivos de
implementação de um módulo: uma implementação de módulo deve incluir sua própria
especificação e os cabeçalhos secundários privados
(consulte "Colocar Especificações de Módulo e Implementações em Arquivos Separados").
// A especificação do módulo foo em seu arquivo de cabeçalho // "foo.hh" // class foo { ... declarations }; // Fim de "foo.hh" // A implementação do módulo foo no arquivo "foo.cc" // #include "foo.hh" // The implementation includes its own // especificação ... definições para os membros foo // Fim de "foo.cc"
Uma exceção à regra #include
ocorre quando um módulo apenas utiliza
ou contém os tipos de um módulo do fornecedor (classes) por referência (utilizando o
ponteiro ou declarações de tipo de referência); nesse caso o uso por referência ou
detenção é especificado utilizando uma declaração de redirecionamento (consulte também
"Minimizar Dependências de Compilação") em vez de uma diretiva
#include
.
Evite incluir mais do que é absolutamente necessário: isso significa que os cabeçalhos do módulo não devem incluir outros cabeçalhos que são requeridos apenas pela implementação do módulo.
#include "a_supplier.hh" class needed_only_by_reference;// Utilize uma declaração de redirecionamento // para uma classe se só precisarmos de // um ponteiro ou um acesso de referência // a ele. void operation_requiring_object(a_supplier required_supplier, ...); // // A operação que requer um objeto real do fornecedor; portanto, a // especificação do fornecedor deve ser #incluída. void some_operation(needed_only_by_reference& a_reference, ...); // // Alguma operação que precisa apenas de uma referência a um objeto; portanto, // deve ser utilizada uma declaração de redirecionamento para o fornecedor.
Essa regra assegura que:
Quando um módulo tiver várias funções seqüenciais, suas definições devem ser colocadas em um arquivo separado de função-seqüencial-única. O arquivo de função seqüencial deve ser incluído ao final do arquivo de cabeçalho do módulo.
Consulte também "Utilizar um Símbolo de Compilação Condicional
No_Inline
para Contestar a Compilação Seqüencial".
Essa técnica impede que os detalhes de implementação misturem o cabeçalho de um módulo; portanto, preservando uma especificação limpa. Isso também ajuda na redução da replicação do código quando não estiver compilando seqüencialmente: utilizando uma compilação condicional, as funções seqüenciais podem ser compiladas em um único arquivo de objeto em oposição à compilação estática em cada módulo sendo utilizado. De forma correspondente, as definições de função seqüencial não devem ser definidas nas definições de classe, a menos que sejam absolutamente triviais.
Quebre os grandes módulos em várias unidades de conversão para facilitar a remoção de código não referenciado durante o link do programa. As funções do membro que raramente são referenciadas devem ser segregadas em arquivos separados desses que geralmente são utilizados. No extremo, as funções membro individuais podem ser colocadas em seus próprios arquivos [Ellemtel, 1993].
Os linkers não são igualmente capazes de eliminar o código não referenciado dentro de um arquivo de objeto. Quebrar os grandes módulos em vários arquivos permite que esses linkers reduzam os tamanhos do programa executável, eliminando o link dos arquivos inteiros de objetos [Ellemtel, 1993].
Também pode ser válido considerar primeiro se o módulo deve ser quebrado em abstrações menores.
Separe o código dependente de plataforma do código independente de plataforma; isso facilitará a portabilidade. Os módulos dependentes de plataforma devem ter nomes de arquivos qualificados por seus nomes de plataforma para realçar a dependência de plataforma.
SymaLowLevelStuff.hh // "LowLevelStuff" // especificação SymaLowLevelStuff.SunOS54.cc // Implementação SunOS 5.4 SymaLowLevelStuff.HPUX.cc // Implementação HP-UX SymaLowLevelStuff.AIX.cc // Implementação AIX
A partir de um ponto de vista da arquitetura e da manutenção, também é uma boa prática conter as dependências de plataforma em um pequeno número de subsistemas de baixo nível.
Adote uma estrutura de conteúdo de arquivo padrão e aplique-a consistentemente
Uma estrutura de conteúdo de arquivo sugerida consiste nas seguintes partes na seguinte ordem:
1. Proteção de inclusão repetida (apenas especificação)
2. Arquivo opcional e identificação de controle de versão.
3. Inclusões de arquivo necessárias por essa unidade.
4. A documentação do módulo (apenas especificação)
5. Declarações (classe, tipo, constantes, objetos e funções) e especificações textuais adicionais (pré-condições e pós-condições e invariantes).
6. Inclusão das definições de função seqüencial desse módulo.
7. Definições (objetos e funções) e declarações privadas da implementação.
8. Aviso de direitos autorais.
9. Histórico do controle de versão opcional.
A ordem do conteúdo de arquivo acima apresenta primeiro as informações pertinentes do cliente e é consistente com o racional para a ordem de seções públicas, protegidas e privadas de uma classe.
Dependendo da política corporativa, as informações de direitos autorais talvez precisem ser colocadas na parte superior do arquivo.
A inclusão repetida de arquivo e a compilação devem ser evitadas utilizando a seguinte construção em cada arquivo de cabeçalho:
#if !defined(module_name) // Utilize os símbolos do pré-processador para #define module_name // se proteger das inclusões // repetidas... // As declarações ficam aqui #include "module_name.inlines.cc" // Optional inline // a inclusão fica aqui. // Sem mais declarações após a inclusão das funções seqüenciais // do módulo. #endif // End of module_name.hh
Utilize o nome de arquivo do módulo para o símbolo de proteção de inclusão. Utilize o mesmo tipo de letra utilizado para o nome do módulo.
No_Inline
" para contestar a
compilação seqüencial Utilize a seguinte construção de compilação condicional para controlar a seqüência versus compilação fora de linha das funções seqüenciáveis:
// Na parte superior de module_name.inlines.hh #if !defined(module_name_inlines) #define module_name_inlines #if defined(No_Inline) #define inline // Anular palavra-chave seqüencial #endif ... // As definições seqüenciais ficam aqui #endif // End of module_name.inlines.hh // No fim de module_name.hh // #if !defined(No_Inline) #include "module_name.inlines.hh" #endif // Na parte superior de module_name.cc após a inclusão de // module_name.hh // #if defined(No_Inline) #include "module_name.inlines.hh" #endif
A construção de compilação condicional é semelhante a várias
construções de proteção de inclusão. Se o símbolo No_Inline
não for
definido, as funções seqüenciais são compiladas com a especificação do módulo
e automaticamente excluídas da implementação do módulo. Se o símbolo
No_Inline
for definido, as definições seqüenciais
serão excluídas da especificação do módulo, mas incluídas na implementação de
módulo com a palavra-chave inline
anulada.
A técnica acima permite a replicação de código reduzido quando as funções seqüenciais forem compiladas fora de linha. Ao utilizar a compilação condicional, uma única cópia das funções seqüenciais é compilada no módulo de definição; versus o código replicado, compilado como funções "estáticas" (link interno) em cada módulo de uso quando a compilação fora de linha for especificada por um comutador do compilador.
O uso da compilação condicional aumenta a complexidade envolvida na manutenção das dependências de construção. Essa complexidade é gerenciada tratando sempre dos cabeçalhos e das definições de função seqüencial como uma única unidade lógica: portanto, os arquivos de implementação dependem dos arquivos de definição de função do cabeçalho e seqüencial.
O recuo consistente deve ser utilizado para delinear visualmente as instruções aninhadas; o recuo entre 2 e 4 espaços foi comprovado por ser visualmente mais efetivo para esse propósito. Recomendamos utilizar um recuo regular de 2 espaços.
Os delimitadores ({}
) de instrução de bloco ou compostos devem estar no
mesmo nível de recuo das instruções vizinhas (por implicação, isso significa que
{}
estão alinhadas verticalmente). As instruções dentro do bloco devem
ser recuadas pelo número escolhido de espaços.
Os etiquetas de caso de uma instrução switch
devem estar no mesmo nível
de recuo da instrução switch
; as instruções dentro da instrução
switch
podem então ser recuadas por 1 nível de recuo da
própria instrução switch
e etiquetas de caso.
if (true) { // Novo bloco foo(); // Instrução(ões) no bloco // recuada(s) em 2 espaços. } else { bar(); } while (expression) { statement(); } switch (i) { case 1: do_something();// Instruções recuadas por // 1 nível de recuo da break; // própria instrução switch. case 2: //... default: //... }
Um recuo de 2 espaços consiste em permitir fácil reconhecimento dos blocos e permitir blocos aninhados suficientes antes que o código se movimente muito fora da borda direita de um monitor de exibição ou página impressa.
Se uma declaração da função não couber em uma única linha, coloque o primeiro parâmetro na mesma linha do nome da função; e cada parâmetro subseqüente em uma nova linha, recuado no mesmo nível do primeiro parâmetro. Esse estilo de declaração e recuo, mostrado abaixo, deixa espaços em branco abaixo do tipo de retorno da função e nome; portanto, melhorando suas visibilidades.
void foo::function_decl( some_type first_parameter, some_other_type second_parameter, status_type and_subsequent);
Se seguir a diretriz acima provocar uma quebra de linha ou fizer com que os parâmetros fiquem com excessivo recuo, recue todos os parâmetros do nome da função ou nome escopo (classe, espaço do nome), sendo cada um em uma linha separada:
void foo::function_with_a_long_name( // o nome da função é muito menos visível some_type first_parameter, some_other_type second_parameter, status_type and_subsequent);
Consulte também as regras de alinhamento abaixo.
O comprimento máximo das linhas do programa deve ser limitado para impedir a perda das informações quando impressas no tamanho de papel impresso padrão (carta) ou padrão.
Se o nível de recuo fizer com que as instruções profundamente aninhadas se movimentem muito para direita e as instruções se estendam muito além da margem direita, provavelmente esse será um bom momento para considerar a quebra do código em funções menores e mais gerenciáveis.
Quando as linhas de parâmetro nas declarações de função, definições e chamadas ou enumeradores em uma declaração enum não couber em uma única linha, quebre a linha após cada elemento da lista e coloque cada elemento em uma linha separada (consulte também "Recuar Parâmetros de Função do Nome da Função ou Nome do Escopo").
enum color { red, orange, yellow, green, //... violet };
Se uma declaração de gabarito de classe ou de função for excessivamente longa, dobre-a em linhas consecutivas após a lista de argumentos de gabarito. Por exemplo, (declaração da Biblioteca de Iteradores padrão, [X3J16, 95]):
template <class InputIterator, class Distance> void advance(InputIterator& i, Distance n);
Esse capítulo fornece orientação sobre o uso de comentários no código.
Os comentários devem ser utilizados para complementar o código fonte, nunca o parafraseie:
Para cada comentário, o programador deve ser capaz de responder facilmente à pergunta: "Qual valor é incluído por esse comentário?" Geralmente, os nomes bem escolhidos eliminam a necessidade dos comentários. Os comentários, a menos que participem de alguma PDL (Program Design Language) formal, não são verificados pelo compilador; portanto, de acordo com o princípio de ponto-único-de-manutenção, as decisões de design devem ser expressas no código fonte em vez de serem expressas em comentários, mesmo nos gastos de algumas declarações a mais.
O delimitador de comentário "//
" de estilo C++
deve ser utilizado na preferência ao "/*...*/
" de estilo C.
Os comentários de estilo C++ são mais visíveis e reduzem o risco de comentar acidentalmente vastas extensões de código devido à falta de um delimitador de fim de comentário.
/* início do comentário com delimitador ausente de fim de comentário do_something(); do_something_else(); /* Comente do_something_else */ // O fim do comentário é aqui ---> */ // do_something e // do_something_else // foram comentados acidentalmente! Do_further();
Os comentários devem ser colocados próximo ao código sobre os quais estão sendo comentados; com o mesmo nível de recuo e anexados ao código utilizando uma linha de comentário em branco.
Os comentários que se aplicam a várias instruções sucessivas de origem devem ser colocados acima das instruções-servindo como uma introdução para as instruções. Da mesma forma, os comentários associados às instruções individuais devem ser colocados abaixo das instruções.
// Um comentário de pré-instruções aplicável // e inúmeras das seguintes instruções // ... void function(); // // Um comentário de pós-instrução para // a instrução precedenten.
Evite comentários na mesma linha de uma construção de origem: eles geralmente ficam desalinhados. No entanto, esses comentários são tolerados para descrições dos elementos em longas declarações, como enumeradores em uma declaração enum.
Evite o uso dos cabeçalhos que contenham informações como autor, números de telefone, datas de criação e de modificação: o autor e os números de telefone se tornam obsoletos rapidamente; ao passo que as datas de criação e de modificação e as razões da modificação são mantidas de maneira mais eficiente por uma ferramenta de gerenciamento de configuração (ou alguma outra forma de arquivo de histórico de versão).
Evite o uso das barras verticais, quadros ou caixas fechadas, mesmo para construções maiores (como funções e classes); eles devem incluir ruído visual e são difíceis de serem mantidos consistentes. Utilize as linhas em branco para separar os blocos relacionados do código fonte em vez das pesadas linhas de comentário. Utilize uma única linha em branco para separar as construções dentro das funções ou classes. Utilize as linhas duplas em branco para separar as funções entre si.
Os quadros ou formulários podem ter a aparência de uniformidade e lembrar o programador para documentar o código, mas eles geralmente conduzem a um estilo de paráfrase [Kruchten, 94].
Utilize comentários vazios, em vez de linhas vazias, dentro de um único bloco de comentário para separar os parágrafos
// Alguma explicação aqui precisa de ser continuada // em um parágrafo subseqüente. // // A linha de comentário vazia acima torna // claro que esse é outro parágrafo // do mesmo bloco de comentário.
Evite repetir identificadores do programa em comentários e replicar as informações encontradas em qualquer lugar-em vez disso, forneça um ponteiro às informações. Caso contrário, qualquer mudança do programa pode precisar de manutenção em vários locais. E a falha ao efetuar as mudanças necessárias no comentário resultará em engano ou comentários errados: sendo que esses encerramentos serão piores do que não ter nenhum comentário.
Sempre vise escrever o código de auto-documentação em vez de fornecer os comentários. Isso pode ser alcançado escolhendo nomes melhores; utilizando variáveis temporárias extras; ou reestruturando o código. Cuidado com o estilo, a sintaxe e a ortografia nos comentários. Utilize comentários do idioma natural em vez do estilo telegráfico ou oculto.
do { ... } while (string_utility.locate(ch, str) != 0); // Saia do loop de procura ao encontrá-lo.por:
do { ... found_it = (string_utility.locate(ch, str) == 0); } while (!found_it);
Embora o código de auto-documentação seja preferido sobre os comentários; geralmente há uma necessidade de fornecer informações além de uma explicação das partes complicadas do código. As informações necessárias são a documentação de, no mínimo, o seguinte:
A documentação do código em conjunto com as declarações deve
ser suficiente para que um cliente utilize o código; a documentação é requerida
desde que as semânticas completas das classes, funções, tipos e objetos
não possam ser totalmente expressas utilizando o C++ sozinho.
Este capítulo fornece orientação sobre a opção dos nomes para as diversas entidades C++.
Aparecer com bons nomes para as entidades do programa (classes, funções, tipos, objetos, literais, exceções e espaços de nomes) não é uma tarefa fácil. Para aplicativos de médio a grande porte, o problema se torna ainda mais desafiador: aqui o nome entra em conflito e a falta de sinônimos para designar conceitos distintos, mas semelhantes, acrescenta o grau de dificuldade.
Utilizar uma convenção de nomenclatura pode diminuir o esforço mental requerido para inventar nomes adequados. Além desse benefício, uma convenção de nomenclatura possui o benefício adicional de reforçar a consistência no código. Para ser útil, a convenção de nomenclatura deve fornecer orientação sobre: o estilo tipográfico (ou como gravar os nomes); e a construção do nome (ou como escolher os nomes).
Não é tão importante qual convenção de nomenclatura será utilizada contanto que seja aplicada consistentemente. A uniformidade na nomenclatura é muito mais importante do que a convenção real: a uniformidade suporta o princípio da surpresa mínima.
Como o C++ é uma linguagem que faz distinção entre maiúsculas e minúsculas e como inúmeras convenções de nomenclatura distintas estão amplamente em uso pela comunidade C++; raramente será possível alcançar a consistência de nomenclatura absoluta. Recomendamos selecionar uma convenção de nomenclatura para o projeto com base no ambiente de host (por exemplo, UNIX ou Windows) e as bibliotecas de princípio utilizadas pelo projeto; para maximizar a consistência do código:
O leitor cuidadoso observará que os exemplos nesse texto atualmente não seguem todas as orientações. Isso é atribuído em parte ao fato de os exemplos serem derivados de várias origens; e também devido ao desejo de conservar o papel; portanto, as orientações de formatação não foram minuciosamente aplicadas. Mas, a mensagem é "faça o que eu digo, mas não o que eu faço".
Os nomes com um único sublinhado inicial ('_') são utilizados geralmente pelas
funções de biblioteca ("_main
" e "_exit
").
Os nomes com sublinhados iniciais duplos ("__"); ou um único sublinhado
inicial seguido de uma letra maiúscula são reservados para uso interno do compilador.
Além disso, evite nomes com sublinhados adjacentes, uma vez que geralmente é difícil discernir o número exato de sublinhados.
É difícil lembrar as diferenças entre os nomes de tipo que diferem apenas por tipo de letra e, portanto, que são fáceis de serem confundidos entre si.
As abreviações podem ser utilizadas se forem utilizadas comumente no domínio do aplicativo (por exemplo, FFT para Fast Fourier Transform), ou se forem definidas em uma lista reconhecida pelo projeto de abreviações. Caso contrário, é muito provável que ocorram aqui abreviações semelhantes, mas não idênticas, introduzindo problemas e erros posteriormente (por exemplo, track_identification sendo abreviado como trid, trck_id, tr_iden, tid, tr_ident e assim por diante).
O uso dos sufixos para categorizar os tipos de entidades (como type para o tipo, e error para as exceções) geralmente não é muito efetivo para transmitir o entendimento do código. Sufixos como array e struct também implicam uma implementação específica; que, no caso de uma mudança na implementação (mudando a representação de uma struct ou array) teria um efeito adverso após qualquer código cliente ou seria equivocado.
No entanto, os sufixos podem ser úteis em inúmeras situações limitadas:
Escolha nomes da perspectiva de uso; e utilize os adjetivos com nomes para aprimorar o significado local (específico de contexto). Além disso, certifique-se de que os nomes concordam com seus tipos.
Escolhas os nomes de modo que construções como:
object_name.function_name(...); object_name->function_name(...);
sejam fáceis de serem lidos e pareçam significativos.
A velocidade da digitação não é uma justificativa aceitável para utilizar nomes resumidos ou abreviados. Os identificadores de uma letra ou abreviados geralmente são uma indicação de opção fraca ou lentidão. As exceções são instâncias bem-reconhecidas como o uso de E para a base dos logaritmos naturais; ou Pi.
Infelizmente, os compiladores e as ferramentas de suporte, às vezes, limitam o comprimento dos nomes; portanto, deve se tomar cuidado para assegura que nomes extensos não sejam diferentes apenas por seus caracteres finais: os caracteres de diferenciação devem ser truncados por essas ferramentas.
void set_color(color new_color) { ... the_color = new_color; ... }is better than:
void set_foreground_color(color fg)and:
oid set_foreground_color(color foreground);{ ... the_foreground_color = foreground; ... }
A nomenclatura no primeiro exemplo é superior aos outros dois: new_color
é qualificada e concorda com seu tipo; através disso, reforçando as semânticas da
função.
No segundo caso, o leitor intuitivo poderia inferir que fg
pretendia significar primeiro plano; no entanto, em qualquer estilo de programação,
nada deve ser deixado para a intuição ou dedução do leitor.
No terceiro caso, quando um parâmetro foreground
for utilizado
(longe de sua declaração), o leitor será levado a acreditar que
foreground
significa, na realidade, a cor do primeiro plano.
No entanto, de forma concebível, poderia ter sido de qualquer tipo
implicitamente conversível para uma color
.
Formar os nomes a partir de substantivos e adjetivos, e assegurar que os nomes concordem com seus tipos segue a linguagem natural e aprimora a legibilidade do código e as semânticas.
As partes dos nomes que são palavras em inglês devem ser escritas corretamente e em conformidade com a forma requerida pelo projeto, ou seja, consistentemente em inglês ou americano, mas não nas duas formas. Isso é igualmente verdadeiro para os comentários.
Para os objetos Booleanos, as funções e argumentos de função, utilize uma
cláusula de predicado na forma positiva, por exemplo,
found_it
, is_available
, mas não is_not_available
.
Ao negar os predicados, os negativos duplos são difíceis de serem entendidos.
Se um sistema for decomposto em subsistemas, utilize os nomes de subsistemas como os nomes de espaço de nomes para particionar e minimizar o espaço de nome global do sistema. Se o sistema for uma biblioteca, utilize um único espaço de nome mais externo para a biblioteca inteira.
Atribua a cada subsistema ou espaço de nome de biblioteca um nome significativo;
além disso, atribua a ele um alias abreviado ou alias de acrônimo. Escolha
aliases abreviados ou de acrônimos que não tenham tendência a entrarem em conflito,
por exemplo, a biblioteca padrão do rascunho ANSI C++ [Plauger,
95] define std
como o alias para iso_standard_library
.
Se o compilador ainda não suportar a construção do espaço de nomes, utilize os prefixos de nome para simular os espaços de nome. Por exemplo, os nomes públicos na interface de um subsistema de gerenciamento de sistemas poderiam ser prefixados com syma (abreviação para Gerenciamento do Sistema).
Utilizar os espaços de nome para cercar os nomes potencialmente globais ajuda a evitar as colisões de nomes quando o código for desenvolvido independentemente (por equipes de subprojetos ou fornecedores). Uma conclusão é que apenas os nomes de espaço de nomes são globais.
Utilize um substantivo ou uma frase substantiva comuns no singular, para atribuir a uma classe um nome que expresse sua abstração. Utilize nomes mais gerais para as classes básicas e nomes mais especializados para as classes derivadas.
typedef ... reference; // A partir da biblioteca padrão typedef ... pointer; // A partir da biblioteca padrão typedef ... iterator; // A partir da biblioteca padrão class bank_account {...}; class savings_account : public bank_account {...}; class checking_account : public bank_account {...};
Quando houver um conflito ou ausência de nomes adequados para ambos, objetos e
tipos; utilize o nome simples para o objeto e inclua um sufixo como
mode, kind, code,
e assim por diante para o nome do tipo.
Utilize uma forma plural ao expressar uma abstração que represente uma coleta de objetos.
typedef some_container<...> yellow_pages;
Quando as semânticas adicionais forem requeridas apenas além de uma coleta de objetos, utilize o seguinte a partir da biblioteca padrão como padrões comportamentais e sufixos de nomes:
Utilize os verbos e as frases de ação para funções que não possuam valores de retorno (declarações de função com um tipo de retorno nulo) ou funções que retornam valor por parâmetros de referência ou ponteiro.
Utilize nomes ou substantivos para funções que retornam apenas um único valor por tipo de retorno de função não nula.
Para classes com operações comuns (um padrão de comportamento), utilize os nomes de operação retirados de uma lista de opções do projeto. Por exemplo: begin, end, insert, erase (operações do contêiner a partir da biblioteca padrão).
Evite a mentalidade de nomenclatura "get" e "set" (funções de prefixos com "get" e "set"), especialmente para as operações públicas para os atributos de objeto de obtenção e configuração. A nomenclatura da operação deve permanecer na abstração da classe e na provisão do nível de serviço; os atributos de objeto de obtenção e configuração são detalhes de implementação de baixo nível que enfraquecem o encapsulamento, se forem tornados públicos.
Utilize os adjetivos (ou particípios passados) para as funções que retornam um Booleano (predicados). Para os predicados, geralmente é útil incluir o prefixo is ou has antes que um substantivo torne a leitura do nome uma asserção positiva. Isso também será útil quando o nome simples já for utilizado para um objeto, nome do tipo ou literal de enumeração. Seja exato e consistente com respeito ao tempo verbal.
void insert(...); void erase(...); Name first_name(); bool has_first_name(); bool is_found(); bool is_available();
Não utilize nomes negativos uma vez que isso pode resultar em expressões com
duplas negações (por exemplo, !is_not_found
); dificultando mais o
entendimento do código. Em alguns casos, um predicado negativo também pode se tornar
positivo sem alterar sua semântica, utilizando um antônimo, como
"is_invalid
" em vez de "is_not_valid
".
bool is_not_valid(...); void find_client(name with_the_name, bool& not_found);Should be re-defined as:
bool is_valid(...); void find_client(name with_the_name, bool& found);
Quando as operações tiverem o mesmo propósito pretendido, utilize a sobrecarga em vez de tentar encontrar sinônimos: isso minimiza o número de conceitos e variações de operações no sistema e, portanto, reduz sua complexidade geral.
Ao sobrecarregar os operadores, certifique-se de que as semânticas do operador sejam preservadas; se o significado convencional de um operador não puder ser preservado, escolha outro nome para a função em vez de sobrecarregar o operador.
Para indicar exclusividade ou para mostrar que essa entidade
é o principal foco da ação, coloque o prefixo
do objeto ou do nome do parâmetro com "the
" ou
"this
". Para indicar um objeto secundário, temporário,
auxiliar, coloque nele o prefixo "a
" ou
"current
":
void change_name( subscriber& the_subscriber, const subscriber::name new_name) { ... the_subscriber.name = new_name; ... } void update(subscriber_list& the_list, const subscriber::identification with_id, structure& on_structure, const value for_value); void change( object& the_object, const object using_object);
Como as exceções devem ser utilizadas apenas para manipular as situações de erro, utilize um substantivo ou uma frase substantiva que transmita claramente uma idéia negativa:
overflow, threshold_exceeded, bad_initial_value
Utilize uma das seguintes palavras bad, incomplete, invalid, wrong, missing ou illegal a partir de uma lista concordada de projetos como parte do nome em vez de utilizar sistematicamente error ou exception, que não transmitem as informações específicas.
A letra 'E' nos literais de ponto flutuante e os dígitos hexadecimais 'A' para 'F' sempre devem ser em maiúsculas.
Este capítulo fornece orientação sobre o uso e a forma de diversos tipos de declarações C++.
Antes da existência do recurso de espaço de nome na linguagem C++, havia apenas meios limitados para gerenciar o escopo do nome; conseqüentemente, o espaço de nome global se tornou ainda mais populado, conduzindo a conflitos que evitaram que algumas bibliotecas fossem utilizadas no mesmo programa. O novo recurso de linguagem de espaço de nomes resolve o problema de poluição de espaço de nome global.
Isso significa que apenas os nomes de espaço de nomes podem ser globais; todas as outras declarações devem estar dentro do espaço de algum espaço de nomes.
Ignorar essa regra pode conduzir eventualmente à colisão de nomes.
Para o agrupamento lógico da funcionalidade não-classe (como uma categoria de classe), ou para funcionalidade com escopo muito maior do que uma classe, como uma biblioteca ou um subsistema; utilize um espaço de nome para unificar logicamente as declarações (consulte "Utilizar Espaços de Nome Para Particionar Nomes Potencialmente Globais por Subsistemas ou por Bibliotecas").
Expresse o agrupamento lógico da funcionalidade no nome.
namespace transport_layer_interface { /* ... */ }; namespace math_definitions { /* ... */ };
O uso de dados de escopo global e do espaço de nomes é contrário ao princípio de encapsulamento.
As classes são o design fundamental e a unidade de implementação no C++. Eles devem ser utilizados para capturar o domínio e as abstrações de design e como um mecanismo de encapsulamento para implementar os ADT (Abstract Data Types).
class
em vez de struct
para implementar os tipos de dados abstratos Utilize a chave de classe class
em vez de struct
para
implementar uma classe-um tipo de dados abstrato.
Utilize a chave de classe struct
para definir estruturas de dados
antigas simples (POD) como em C, especialmente ao fazer interface com o código C.
Embora class
e struct
sejam equivalentes e possam ser
utilizados de forma permutável, o class
possui ênfase de controle de
acesso padrão preferencial (privado) para melhor encapsulamento.
Adotar uma prática consistente para distinguir entre class
e struct
introduz uma distinção semântica acima e além das regras de linguagem: a class
se torna a construção principal para capturar as abstrações e o encapsulamento; enquanto que struct
representa uma estrutura de dados pura que pode ser trocada em programas de linguagem de programação mista.
Os especificadores de acesso em uma declaração de classe devem aparecer na ordem publica, protegida, privada.
A ordem pública, protegida e privada das declarações do membro assegura que as informações de maior interesse para o usuário da classe sejam apresentadas primeiro, reduzindo assim, a necessidade para o usuário da classe navegar através de detalhes de implementação ou irrelevantes.
O uso dos membros de dados públicos ou protegidos reduz o encapsulamento de uma classe e afeta a elasticidade de um sistema a ser alterado: os membros de dados públicos expõem uma implementação class' aos seus usuários; os membros de dados protegidos expões uma implementação class' a suas classes derivadas. Qualquer alteração nos membros de dados protegidos ou públicos de class' terá conseqüências sobre os usuários e as classes derivadas.
Essa orientação parece ser não intuitiva à primeira vista: a amizade expõe as partes privadas aos amigos; portanto, como pode ser preservado o encapsulamento? Nas situações em que as classes são altamente interdependentes e requerem conhecimento interno de cada um, é melhor manter a boa relação do que exportar os detalhes internos através da interface da classe.
Exportar os detalhes internos como membros públicos concede acesso aos clientes que não são desejáveis. Exportar os membros protegidos concede acesso a descendentes potenciais, encorajando um design hierárquico que também não é desejável. A boa relação concede acesso privado seletivo sem reforçar uma restrição de subclasse; portanto, preservando o encapsulamento de todos, mesmo daqueles que requerem acesso.
Um bom exemplo de uso da boa relação para preservar o encapsulamento é conceder a boa relação a uma classe de teste de amigos. A classe de teste de amigos, analisando a parte interna da classe, pode implementar o código de teste apropriado, mas posteriormente, a classe de teste de amigos pode ser eliminada do código fornecido. Portanto, nenhum encapsulamento será perdido nem codificado em adição ao código distribuível.
As declarações de classe devem conter apenas as declarações de função e nunca as definições de função (implementações).
Fornecer as definições de função em uma declaração de classe polui a especificação da classe com detalhes de implementação; tornando a interface de classe menos compreensível e mais difícil de ser lida; e aumenta as dependências de compilação.
As definições de função nas declarações da classe também reduzem o controle sobre a
seqüência da função (consulte também "Utilizar um Símbolo de Compilação Condicional
No_Inline
para Contestar a Compilação Seqüencial").
Para permitir o uso de uma classe em uma matriz, ou de qualquer um dos contêineres STL; uma classe deve fornecer um construtor padrão público ou permitir a geração de um pelo compilador.
Existe uma exceção à regra acima quando uma classe tiver um membro de dados não estáticos do tipo de referência, nesse caso geralmente não será possível criar um construtor padrão significativo. Portanto, é questionável utilizar uma referência para um membro de dados do objeto.
Se necessário e não declarado explicitamente, o compilador gerará implicitamente um construtor de cópia e um operador de designação para uma classe. O construtor de cópia definido pelo compilador e o operador de designação implementam o que é conhecido geralmente na terminologia Smalltalk como "shallow-copy": explicitamente, cópia compreensiva de membro com cópia bit a bit para ponteiros. O uso do construtor de cópia gerado pelo compilador e operadores de designação padrão é garantido para fuga de memória.
// Adapted from [Meyers, 92]. void f() { String hello("Hello");// Assume String is implemented // with a pointer to a char // array. { // Enter new scope (block) String world("World"); world = hello; // Assignment loses world's // original memory } // Destruct world upon exit from // block; // also indirectly hello String hello2 = hello; // Assign destructed hello to // hello2 }
No código acima, a memória que retém a cadeia "World
"
é perdida após a designação. Após sair do bloco interno, world
é destruído; portanto, perdendo também a memória referenciada por hello
. O
hello
destruído é designado ao hello2
.
// Adapted from [Meyers, 1992]. void foo(String bar) {}; void f() { String lost = "String that will be lost!"; foo(lost); }
No código acima, quando foo
é chamado com o argumento lost
,
lost
será copiado para o foo
utilizando o construtor de
cópia definido pelo compilador. Como lost
é copiado com uma cópia
bitwise do ponteiro para "String that will be lost!"
, após
sair de foo
, a cópia lost
será destruída (assumindo que o
destruidor será implementado corretamente para liberar memória) juntamente com a
memória que retém "String that will be lost!"
// Example from [X3J16, 95; section 12.8] class X { public: X(const X&, int); // int parameter is not // initialized // No user-declared copy constructor, thus // compiler implicitly declares one. }; // Deferred initialization of the int parameter mutates // constructor into a copy constructor. // X::X(const X& x, int i = 0) { ... }
Um compilador que não vê uma assinatura de construtor de cópia "standard" em uma declaração de classe declarará implicitamente um construtor de cópia. No entanto, a inicialização adiada dos parâmetros padrão pode modificar um construtor no construtor de cópia: resultando em ambigüidade quando for utilizado um construtor de cópia. Portanto, qualquer uso de um construtor de cópia será mal formado por causa da ambigüidade [X3J16, 95; seção 12.8].
A menos que uma classe seja explicitamente designada como não derivável, seu destruidor sempre deve ser declarado virtual.
A exclusão de um objeto de classe derivada através de um ponteiro ou referência para um tipo de classe base resultará no comportamento não definido, a menos que o destruidor de classe base tenha sido declarado como virtual.
// Estilo inválido utilizado para redução class B { public: B(size_t size) { tp = new T[size]; } ~B() { delete [] tp; tp = 0; } //... private: T* tp; }; class D : public B { public: D(size_t size) : B(size) {} ~D() {} //... }; void f() { B* bp = new D(10); delete bp; // Comportamento não definido devido ao // destruidor de classe base // não virtual }
Os construtores de parâmetro únicos também podem ser impedidos de serem
utilizados para conversão implícita, declarando-os com o especificador
explicit
.
As funções não virtuais implementam o comportamento invariante e não são destinadas a serem especializadas por classes derivadas. Violar essa orientação pode produzir o comportamento inesperado: o mesmo objeto pode exibir diferente comportamento em diferentes momentos.
As funções não virtuais são estaticamente ligadas; portanto, a função chamada após um objeto é regida pelo tipo estático da variável que faz referência a object-pointer-to-A e pointer-to-B respectivamente no exemplo abaixo-e não ao tipo real do objeto.
// Adapted from [Meyers, 92]. class A { public: oid f(); // Non-virtual: statically bound }; class B : public A { public: void f(); // Non-virtual: statically bound }; void g() { B x; A* pA = &x; // Static type: pointer-to-A B* pB = &x; // Static type: pointer-to-B pA->f(); // Calls A::f pB->f(); // Calls B::f }
Como as funções não virtuais restringem as subclasses, ao restringir a especialização e o polimorfismo, cuidado para assegurar que uma operação seja verdadeiramente invariante para todas as subclasses antes de declará-la não-virtual.
A inicialização do estado de um objeto durante a construção deve ser executada pelo incializador construtor (uma lista do inicializador do membro) em vez de ser com operadores de designação dentro do corpo do construtor.
class X { public: X(); private Y the_y; }; X::X() : the_y(some_y_expression) { } // // "the_y" inicializado por um inicializador construtorRather than this:
X::X() { the_y = some_y_expression; } // // "the_y" initialized by an assignment operator.
A construção do objeto envolve a construção de todas as classes base e membros de dados antes da execução do corpo do construtor. A inicialização de membros de dados requer duas operações (construção mais designação), se executada em um corpo de construtor em oposição a uma única operação (construção com um valor inicial) quando executada utilizando um inicializador de construção.
Para classes agregadas aninhadas maiores (classes que contêm classes que contêm classes...), os códigos extra de desempenho de várias operações-construção + designação de membros-podem ser significativos.
class A { public: A(int an_int); }; class B : public A { public: int f(); B(); }; B::B() : A(f()) {} // undefined: calls member function but A bas // not yet been initialized [X3J16, 95].
O resultado de uma operação será indefinido, se uma função-membro for chamada diretamente ou indiretamente a partir de um inicializador construtor antes que todos os inicializadores membro para as classes base tenham concluído [X3J16, 95].
Cuidado ao chamar as funções membro nos construtores; fique atento pois mesmo se uma função virtual for chamada, aquele que for executado será o definido na classe do construtor ou destruidor ou em uma de suas bases.
static
const
para constantes de classe integralAo definir as constantes de classe integral (inteiro), utilize os membros de dados
static const
em vez de #define
ou de constantes globais. Se
static const
não for suportado pelo compilador, utilize
enum
em substituição.
class X { static const buffer_size = 100; char buffer[buffer_size]; }; static const buffer_size;Or this:
class C { enum { buffer_size = 100 }; char buffer[buffer_size]; };But not this:
#define BUFFER_SIZE 100 class C { char buffer[BUFFER_SIZE]; };
Isso evitará confusão quando o compilador reclamar de um tipo de retorno ausente para as funções declaradas sem um tipo de retorno explícito.
Além disso, utilize os mesmos nomes nas declarações de função e nas definições; isso minimizará as surpresas. Fornecer nomes de parâmetro aprimora a documentação do código e a capacidade de leitura.
As instruções de retorno dispersas livremente sobre um corpo da função são
comparáveis às instruções goto
, dificultando a leitura e a manutenção
do código.
Vários retornos podem ser tolerados apenas em funções muito pequenas, quando todos os
return
s podem ser vistos simultaneamente e quando o código tiver uma
estrutura muito regular:
type_t foo() { if (this_condition) return this_value; else return some_other_value; }
As funções com o tipo de retorno nulo não devem ter instrução de retorno.
A criação de funções que produzem efeitos colaterais globais (mude os dados não publicados em vez de seus estados de objetos internos: como dados globais e de espaço de nome) deve ser minimizada (consulte também "Minimizar o Uso de Dados Globais e Escopo de Espaço de Nome"). Mas se for inevitável, qualquer efeito colateral deve ser claramente documentado como parte da especificação da função.
Transmitir os projetos requeridos como parâmetros torna o código menos dependente de contexto, mais robusto e mais fácil de ser entendido.
A order na qual os parâmetros são declarados é importante do ponto do vista do responsável pela chamada:
Essa classificação permite tirar proveito dos padrões para reduzir o número de argumentos nas chamadas de função.
Os argumentos para as funções com um número variável de parâmetros não podem ser verificados por tipo.
Evite incluir padrões nas funções em novas declarações adicionais da função: além das declarações de redirecionamento, uma função deve ser declarada apenas uma vez. Caso contrário, isso pode causar confusão para os leitores que não estão cientes das declarações subseqüentes.
Verifique se as funções possuem algum comportamento constante (retorne um valor
de constante; aceite os argumentos de constantes; ou opere sem efeitos colaterais)
e expresse o comportamento utilizando o especificador const
.
const T f(...); // Function returning a constant // object. T f(T* const arg); // Function taking a constant // pointer. // The pointed-to object can be // changed but not the pointer. T f(const T* arg); // Function taking a pointer to, and T f(const T& arg); // function taking a reference to a // constant object. The pointer can // change but not the pointed-to // object. T f(const T* const arg); // Function taking a constant // pointer to a constant object. // Neither the pointer nor pointed- // to object may change. T f(...) const; // Function without side-effect: // does not change its object state; // so can be applied to constant // objects.
Transmitir e retornar os objetos por valor pode incorrer em código extra do construtor e do destruidor. O código extra do construtor e do destruidor pode ser evitado transmitindo e retornando os objetos, por referência.
As referências Const
podem ser utilizadas para especificar se os
argumentos transmitidos pela referência não podem ser modificadas. Os exemplos
de uso típico são operadores de designação e construtores de cópia:
C::C(const C& aC); C& C::operator=(const C& aC);
Considere o seguinte:
the_class the_class::return_by_value(the_class a_copy) { return a_copy; } the_class an_object; return_by_value(an_object);
Quando return_by_value
for chamado com an_object
como argumento, o construtor de cópia the_class
é chamado para copiar
an_object
para a_copy
. O construtor de
cópia the_class
é chamado novamente para copiar
a_copy
para o objeto temporário de retorno de função.
O destruidor the_class
é chamado para destruir
a_copy
após o retorno da função.
Em algum tempo posteriormente, o destruidor the_class
será chamado
novamente para destruir o objeto retornado por return_by_value
. O custo
geral da chamada de função do-nothing é de dois construtores e de dois destruidores.
A situação será ainda pior se the_class
era uma classe derivada e
continha dados de membro de outras classes; os construtores e destruidores das
classes bases e classes contidas também seriam chamados; portanto, escalar o número
de chamadas de construtores e destruidores incorridos pela chamada de função.
A orientação acima pode parecer convidar os desenvolvedores para sempre transmitirem e retornarem os objetos por referência; no entanto, deve-se tomar cuidado para não retornar referências aos objetos locais ou referências quando os objetos forem necessário. Retornar uma referência para um objeto local é um convite para o desastre após o retorno da função, a referência retornada tende a um objeto destruído!
Os objetos locais são destruídos ao sair do escopo de função; utilizar objetos destruído é um convite ao desastre.
A violação dessa orientação conduzirá a fugas de memória
class C { public: ... friend C& operator+( const C& left, const C& right); }; C& operator+(const C& left, const C& right) { C* new_c = new C(left..., right...); return *new_c; } C a, b, c, d; C sum; sum = a + b + c + d;
Como os resultados intermediários dos operadores + não são armazenados ao calcular a soma, os objetos intermediários não podem ser excluídos, conduzindo a fugas de memória.
A violação dessa orientação viola o encapsulamento de dados e pode conduzir a surpresas ruins.
#define
para a expansão da macro Mas utilize inlining de forma sensata: apenas para funções bem pequenas; grandes funções inlining podem provocar a saturação do código.
As funções seqüenciais também aumentam as dependências de compilação entre os módulos, uma vez que a implementação das funções seqüenciais precisam se tornar disponíveis para compilação do código do cliente.
[Meyers, 1992] fornece uma discussão detalhada do seguinte, e não do exemplo extremo do uso incorreto da macro:
Não execute:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Em vez disso, execute:
inline int max(int a, int b) { return a > b ? a : b; }
A macro MAX
possui inúmeros problemas: ela não é type-safe; e seu
comportamento é não determinista:
int a = 1, b = 0; MAX(a++, b); // a é incrementado duas vezes MAX(a++, b+10); // a é incrementado uma vez MAX(a, "Hello"); // comparando ints com ponteiros
Utilize os parâmetros padrão em vez da sobrecarga de funções quando um único algoritmo puder ser explorado e o algoritmo puder ser colocado em parâmetro por um pequeno número de parâmetros.
Utilizar os parâmetros padrão ajuda a reduzir o número de funções sobrecarregadas, aprimorando a capacidade de manutenção e reduz o número de argumentos necessários nas chamadas de função, aprimorando a capacidade de leitura do código.
Utilize a sobrecarga de função quando várias implementações forem requeridas para a mesma operação semântica, mas com diferentes tipos de argumentos.
Preserve o significado convencional ao sobrecarregar os operadores. Não se esqueça
de definir os operadores relacionados, por exemplo
operator==
e operator!=
.
Evite sobrecarregar funções com um único argumento de ponteiro, por funções com um único argumento de inteiro:
void f(char* p); void f(int i);
As seguintes chamadas podem provocar surpresas:
f(NULL); f(0);
A resolução de sobrecarga resolve f(int)
e não f(char*)
.
operator=
retorne uma referência para *this
C++ permite o encadeamento das operações de designação:
String x, y, z; x = y = z = "A string";
Como o operador de designação é associativo à direita, a cadeia "A
string
" é designada para z, z para y, e y para x. O operator=
é chamado efetivamente uma vez para cada expressão à direita de
=, em uma ordem da direta para a esquerda. Isso também significa que o resultado de cada
operator=
é um objeto; no entanto, é possível uma opção de retorno do
objeto da mão esquerda ou da mão direita.
Como a boa prática dita que a assinatura do operador de designação sempre deve ser no formato:
C& C::operator=(const C&);
apenas o objeto da mão esquerda é possível (rhs é a referência
const, lhs é a referência non-const); portanto, *this
deve ser
retornado. Consulte [Meyers, 1992] para obter uma
discussão detalhada.
operator=
verifique a auto-designaçãoExistem duas boas razões para executar a verificação: primeiramente, a designação de um objeto de classe derivada envolve chamar o operador de designação de cada classe base até a hierarquia de herança e pular essas operações pode proporcionar significativas economias no tempo de execução. Em segundo lugar, a designação envolve a destruição do objeto "lvalue" antes de copiar o objeto "rvalue". No caso de uma auto-designação, o objeto rvalue é destruído antes de ser designado, o resultado da designação é então indefinida.
Não grave funções excessivamente longas, por exemplo, com mais de 60 linhas de código.
Minimize o número de instruções de retorno, 1 é o número ideal.
Tente uma Complexidade Ciclomática inferior a 10 (soma das instruções de decisão + 1, para funções de instrução de saída simples).
Tente uma Complexidade Ciclomática Estendida inferior a 15 (soma das instruções de decisão + operadores lógicos + 1, para funções únicas de instrução de saída).
Minimize a extensão máxima da média da referência (distância em linhas entre a declaração de um objeto local e a primeira instância de seu uso).
Em grandes projetos, geralmente há uma coleta dos tipos utilizados freqüentemente em todo o sistema; nesse caso, é sensato coletar em conjunto esses tipos em um ou mais espaços de nome de utilitário global de baixo nível (consulte o exemplo para "Evitar o Uso de Tipos Fundamentais").
Quando um alto grau de portabilidade for o objetivo, ou quando o controle for necessário sobre o espaço de memória ocupado por objetos numéricos ou quando um intervalo específico de valores for requerido; os tipos fundamentais não devem ser utilizados. Nessas situações é melhor declarar os nomes de tipo explícitos com restrições de tamanho que utilizam os tipos fundamentais apropriados.
Certifique-se para que os tipos fundamentais não escapem de volta no código através de contadores de loop, índices de matriz e assim por diante.
namespace system_types { typedef unsigned char byte; typedef short int integer16; // 16-bit signed integer typedef int integer32; // 32-bit signed integer typedef unsigned short int natural16; // 16-bit unsigned integer typedef unsigned int natural32; // 32-bit unsigned integer ... }
A representação dos tipos fundamentais é dependente da implementação.
typedef
para criar sinônimos
para reforçar o significado local Utilize typedef
para criar sinônimos para os nomes existentes, para
fornecer nomes locais mais significativos e aprimorar a
legibilidade (não há penalidade de tempo de execução para isso).
typedef
também pode ser utilizado para fornecer atalhos para os nomes
qualificados.
// vector declaration from standard library // namespace std { template <class T, class Alloc = allocator> class vector { public: typedef typename Alloc::types<T>reference reference; typedef typename Alloc::types<T>const_reference const_reference; typedef typename Alloc::types<T>pointer iterator; typedef typename Alloc::types<T>const_pointer const_iterator; ... } }
Ao utilize typedef-names criados por typedef
, não misture o uso do
nome original e o sinônimo na mesma parte do código.
Utilize constantes nomeadas na preferência.
Utilize const
ou enum
em substituição.
Não execute:
#define LIGHT_SPEED 3E8Em vez disso, execute:
const int light_speed = 3E8;Ou isso, para as matrizes de redimensionamento:
enum { small_buffer_size = 100, large_buffer_size = 1000 };
A depuração é muito mais difícil porque os nomes introduzidos por #defines
são substituídos durante o pré-processamento da compilação e não
aparecem nas tabelas do símbolo.
Sempre inicialize os
objetos const na declaração
Os objetos const
não declarados extern
possuem link interno,
inicializar esses objetos constantes na declaração permite que os inicializadores
sejam utilizados no momento da compilação.
Os objetos constantes podem existir na memória de leitura.
Especifique os valores iniciais nas definições do objeto, a menos que o objeto seja de auto-inicialização. Se não for possível designar um valor inicial significativo, designe um valor "nil" ou considere declarar o objeto posteriormente.
Para grandes objetos, geralmente não é aconselhável construir os objetos e posteriormente inicializá-los utilizando a designação uma vez que isso pode ser muito oneroso (consulte também "Utilizar os Inicializadores do Construtor em Vez das Designações no Construtor").
Se a inicialização apropriada de um objeto não for possível no momento da construção, inicialize o objeto utilizando um valor "nil" convencional que signifique "uninitialized". O valor nil deve ser utilizado apenas para inicialização para declarar um "unusable but known value" que possa ser rejeitado de uma forma controlada pelos algoritmos: para indicar um erro variável não inicializado quando o objeto for utilizado antes da inicialização apropriada.
Observe que nem sempre é possível declarar um valor nil para todos os tipos, especialmente os tipos de módulo, como um ângulo. Nesse caso, escolha o valor menos provável.
Este capítulo fornece orientação sobre o uso e a forma de diversos tipos de expressões e instruções do C++.
Evite aninhar as expressões muito profundamente
O nível de aninhamento de uma expressão é definido como o número de conjuntos aninhados de parênteses requeridos para avaliar uma expressão da esquerda para a direita, se as regras de precedência do operador forem ignoradas.
Níveis demasiados de aninhamento dificultam a compreensão das expressões.
A menos que a ordem de avaliação seja especificada por um operador (operador de vírgula, expressão ternária e conjunções e disjunções); não assuma qualquer ordem de avaliação específica; assumir pode conduzir a surpresas desagradáveis e à não portabilidade.
Por exemplo, não combine o uso de uma variável na mesma instrução de um incremento ou decremento da variável.
foo(i, i++); array[i] = i--;
NULL
O uso de 0 ou NULL
para ponteiros nulos é um tópico altamente controvertido.
C e C++ definem qualquer expressão constante com valor zero a ser interpretado como um ponteiro nulo. Como
0 é difícil de ser lido e o uso de literais é altamente desencorajado, os
programadores utilizavam tradicionalmente a macro NULL
como o ponteiro
nulo. Infelizmente, não há definição portátil para NULL
.
Alguns compiladores ANSI C utilizavam (void *)0, mas isso se torna uma opção
insuficiente para C++:
char* cp = (void*)0; /* Legal C but not C++ */
Portanto, qualquer definição de NULL
do formato (T*)0, em vez de
zero simplesmente, requer um lançamento em C++. Historicamente, as diretrizes
que defendem o uso de 0 para os ponteiros nulos, tentavam aliviar o requisito de
lançamento e tornam o código mis portátil. No entanto, vários desenvolvedores C++
se sentem mais confortáveis utilizando NULL
em vez de 0 e também argumentam
que a maioria dos compiladores (mais precisamente, a maioria dos arquivos de cabeçalho)
atualmente implementam NULL
como 0.
Essa orientação rege em favor de 0, uma vez que 0 tem a garantia de trabalhar
independentemente do valor NULL
; no entanto, devido à controvérsia,
esse ponto é rebaixado para o nível de uma dica, a ser seguida ou ignorada, conforme
adequado.
Utilize os novos operadores de lançamento (dynamic_cast, static_cast,
reinterpret_cast, const_cast
) em vez do lançamento de estilo antigo.
Se você não possui novos operadores de lançamento; evite lançar em conjunto, especialmente para baixo (convertendo um objeto de classe base para um objeto de classe derivada).
Utilize os operadores de lançamento da seguinte forma:
dynamic_cast
-para lançar entre os membros da mesma hierarquia de
classe (subtipos) utilizando as informações de tipo de tempo de execução
(as informações de tipo de tempo de execução estão disponíveis para as classes com
funções virtuais). O lançamento entre essas classes é garantido por ser seguro. static_cast
-para lançar entre os membros da mesma hierarquia de classe sem
utilizar as informações de tipo de tempo de execução; isso não é garantido por ser seguro. Se
o programador não puder garantir type-safety, execute use dynamic_cast.
reinterpret_cast
-para lançar entre os tipos de ponteiros não
relacionados e os tipos integrais (inteiro); isso não é seguro e deve ser utilizado
apenas entre os tipos mencionados. const_cast
-para lançar "constness" de um argumento de
função especificado como um parâmetro const
. Observe que
const_cast
não destina-se a lançar o
"constness" de um objeto realmente definido como um objeto
const (ele poderia estar em read-only-memory).Não utilize typeid
para implementar a lógica type-switching: deixe
que os operadores de lançamento executem a verificação de tipo e
a conversão de forma atômica, consulte [Stroustrup,
1994] para obter uma discussão aprofundada.
Não execute o seguinte:
void foo (const base& b) { if (typeid(b) == typeid(derived1)) { do_derived1_stuff(); else if (typeid(b) == typeid(derived2)) { do_derived2_stuff(); else if () { } }
O lançamento de estilo antigo destrói o sistema de tipo e pode conduzir a erros hard-to-detect que não são capturados pelo compilador: o sistema de gerenciamento de memória pode ser corrompido, as tabelas de função virtuais podem ser minimizadas e os objetos não relacionados podem ser danificado quando o objeto for acessado como um objeto de classe derivada. Observe que o dano pode ser feito mesmo por um acesso de leitura, uma vez que os ponteiros ou campos não existentes podem ser referenciados.
Os operadores de lançamento de novo estilo tornam a conversão de tipo mais segura (na maioria dos casos) e mais explícita.
Não utilize as constantes ou macros Booleanas de estilo antigo:
não há um valor Booleado padrão true; utilize o novo tipo bool
, em
substituição.
Como tradicionalmente não havia nenhum valor padrão para (1 ou ! 0); as comparações de expressões diferentes de zero para true poderiam falhar.
Em vez disso, utilize as expressões Booleanas.
Evite executar o seguinte:
if (someNonZeroExpression == true) // May not evaluate to trueO melhor é executar:
if (someNonZeroExpression) // Always evaluates as a true condition.
O resultado dessas operações são quase sempre sem sentido.
Evite o desastre configurando um ponteiro para um objeto excluído como nulo: a exclusão repetida de um ponteiro não nulo é prejudicial, mas a exclusão repetida de um ponteiro nulo é inofensiva.
Sempre designe um valor de ponteiro nulo após a exclusão mesmo antes de um retorno de função, uma vez que o novo código pode ser incluído posteriormente.
Utilize uma
switch-statement ao ramificar valores distintos
Utilize uma instrução de comutação em vez de uma série de "else if" quando a condição de ramificação for um valor distinto.
default
para switch-statements para capturar os errosUma instrução de comutação sempre deve conter uma ramificação default
e a ramificação default
deve ser utilizada para erros de interrupção.
Essa política assegura que quando novos valores de comutação são introduzidos e as ramificações para manipular os novos valores são omitidos, a ramificação padrão existente capturará o erro.
Utilize um for-statement em preferência a uma instrução while quando a iteração e a terminação do loop forem baseadas no contador de loop.
Evite o
uso das instruções jump nos loops
Evite sair (utilizando break, return
ou goto
) dos loops
em vez da condição de terminação de loop; e pular de forma prematura para a próxima
iteração com continue
. Isso reduz o número de fluxo de caminhos de
controle, facilitando a compreensão do código.
goto
Isso parece ser uma orientação universal.
Isso pode conduzir à confusão para os leitores e os riscos potenciais na manutenção.
Este capítulo fornece orientação sobre os tópicos de gerenciamento de memória e relatório de erro.
A funções malloc
, calloc
e realloc
da biblioteca C não devem ser utilizadas para alocar o espaço do objeto:
o operador C++ novo deve ser utilizado para esse propósito.
A única vez em que a memória deve ser alocada utilizando as funções C é quando a memória deve ser transmitida a uma função de biblioteca C para descarte.
Não utilize excluir para liberar a memória alocada pelas funções C, ou para liberar os objetos criados por new.
Utilizar delete em objetos de matriz sem a notação de colchetes vazios ("[]") resultará apenas no primeiro elemento de matriz sendo excluído e, portanto, em fuga de memória.
Como não foi obtida muita experiência ao utilizar o mecanismo de exceção C++, as orientações apresentadas aqui podem passar por significativa revisão futura.
O padrão de rascunho C++ define duas amplas categorias de erros: erros lógicos e erros de tempo de execução. Os erros lógicos podem ser erros de programação evitáveis. Os erros de tempo de execução são definidos como erros que são atribuídos aos eventos que ultrapassam o escopo do programa.
A regra geral para o uso das exceções é que o sistema na condição normal e na ausência de sobrecarga ou falha de hardware não deve lançar qualquer exceção.
Utilize as asserções de pré e pós-condição da função durante o desenvolvimento para fornecer detecção de erro "drop-dead".
As asserções fornecem um mecanismo de detecção de erro de provisão simples e útil até que o código de manipulação de erro final seja implementado. As asserções possuem o bônus adicional para que seja capaz de ser compilado utilizando o símbolo do pré-processador "NDEBUG" (consulte "Definir o Símbolo NDEBUG com um Valor Específico").
A macro de asserção foi tradicionalmente utilizada para esse propósito; no entanto, a referência [Stroustrup, 1994] fornece uma alternativa de gabarito, consulte abaixo.
template<class T, class Exception> inline void assert ( T a_boolean_expression, Exception the_exception) { if (! NDEBUG) if (! a_boolean_expression) throw the_exception; }
Não utilize as exceções para os eventos freqüentes antecipados: as exceções provocam interrupções no fluxo normal de controle do código; dificultando ainda mais o entendimento e a manutenção.
Os eventos antecipados devem ser tratados no fluxo normal de controle do código; utilize um valor de retorno da função ou o código de status do parâmetro "out", conforme necessário.
As exceções também não devem ser utilizadas para implementar as estruturas de controle: isso seria outra forma de instrução "goto".
Isso assegura que todas as exceções suportem um conjunto mínimo de operações comuns e possam ser manipuladas por um pequeno conjunto de rotinas de tratamento de nível alto.
Os erros lógicos (erro de domínio, erro de argumento inválido, erro de comprimento e erro fora do intervalo) devem ser utilizados para indicar os erros de domínio no aplicativo, os argumentos inválidos transmitidos às chamadas de função, construção de objetos além de seus tamanhos permitidos e valores de argumento que não estão dentro das faixas permitidas.
Os erros de tempo de execução (erro de faixa e erro de estouro) devem ser utilizados para indicar erros aritméticos e de configuração, dados corrompidos ou erros de esgotamento de recursos detectáveis apenas no tempo de execução.
Em grandes sistemas, ter que manipular um grande número de exceções em cada nível dificulta a leitura e a manutenção do código. O processamento da exceção pode atrapalhar o processamento normal.
As maneiras de minimizar o número de exceções são:
Compartilhar as exceções entre as abstrações, utilizando um pequeno número de categorias de exceção.
Emita as exceções especializadas derivadas das exceções padrão, mas manipule exceções mais generalizadas.
Inclua os estados "exceptional" nos objetos e forneça os primitivos para verificar explicitamente a validade dos objetos.
As exceções originadas das funções (que não transmitem apenas as exceções) devem declarar todas as exceções lançadas em suas especificações de exceção: elas não devem gerar silenciosamente as exceções sem avisar seus clientes.
Durante o desenvolvimento, relate as exceções pelo mecanismo de log apropriado assim que possível, inclusive em "throw-point".
As rotinas de tratamento de exceção devem ser definidas na ordem de classe mais mais derivada para a de maior base para evitar a codificação da rotina de tratamento inacessível; consulte o exemplo how-not-to-it, abaixo. Isso também assegurar que a rotina de tratamento mais apropriada captura a exceção porque as rotinas de tratamento são correspondidas em uma ordem de declaração.
Não execute:
class base { ... }; class derived : public base { ... }; ... try { ... throw derived(...); // // Emita uma exceção de classe derivada } catch (base& a_base_failure) // // Mas a rotina de tratamento de classe base "catches", porque // ela combina primeiro! { ... } catch (derived& a_derived_failure) // // Essa rotina de tratamento é inacessível! { ... }
Evite as rotinas de tratamento de exceção catch-all (declarações de rotina de tratamento que utilizam ...), a menos que a exceção seja emitida novamente.
As rotinas de tratamento catch-all devem ser utilizadas apenas para administração interna local; em seguida, a exceção deve ser emitida novamente para impedir o mascaramento do fato de que a exceção não pode ser manipulada nesse nível:
try { ... } catch (...) { if (io.is_open(local_file)) { io.close(local_file); } throw; }
Ao retornar os códigos de status como um parâmetro de função, sempre designe um valor para o parâmetro como a primeira instrução executável no corpo da função. Sistematicamente, torne todos os status um êxito por padrão ou uma falha por padrão. Considere todas as saídas possíveis da função, incluindo as rotinas de tratamento de exceção.
Se uma função puder produzir uma saída errônea, a menos que seja fornecida uma entrada apropriada, instale o código na função para detectar e relatar a entrada inválida de uma maneira controlada. Não confie em um comentário que informe ao cliente para transmitir os valores apropriados. É virtualmente garantido que, mais cedo ou mais tarde, esse comentário será ignorado, resultando em erros de difícil depuração, se os parâmetros inválidos não forem detectados.
Esse capítulo trata dos recursos de idioma que são uma priori não portáteis.
Os nomes de caminho não são representados de uma maneira padrão nos sistemas operacionais. Utilizá-los introduzirá as dependências de plataforma.
#include "somePath/filename.hh" // Unix #include "somePath\filename.hh" // MSDOS
A representação e o alinhamento dos tipos são altamente dependentes da arquitetura da máquina. As premissas feitas sobre a representação e o alinhamento podem conduzir a surpresas desagradáveis e portabilidade reduzida.
Em específico, nunca tente armazenar um ponteiro em um int
, um tipo
numérico extenso ou qualquer outro tipo numérico - isso é altamente não-portátil.
Não dependa de um determinado comportamento de limite baixo
ou estouro
Utilize constantes
"stretchable" sempre que possível
As constantes flexíveis evitam problemas com as variações dos tamanhos das palavras.
const int all_ones = ~0; const int last_3_bits = ~0x7;
As arquiteturas da máquina podem ditar o alinhamento de determinados tipos. Converter de tipos com requisitos de alinhamento mais relaxados para tipos com requisitos de alinhamento mais severos pode conduzir a falhas no programa.
Esse capítulo fornece orientação sobre a reutilização do código C++.
Se as bibliotecas padrão não estiverem disponíveis, crie as classes com base nas interfaces de biblioteca padrão: isso facilitará a futura migração.
Utilize os gabaritos para reutilizar o comportamento, quando ele não for dependente de um tipo de dados específico.
Utilize a herança public
para expressar o relacionamento "isa"
e reutilizar as interfaces de classe base e, opcionalmente, suas implementações.
Evite a herança privada ao reutilizar a implementação ou modelar os relacionamentos "parciais/inteiros". A reutilização da implementação sem a redefinição é melhor alcançada pela detenção em vez da herança privada.
Utilize a herança privada quando for necessária a redefinição de operações de classe-base.
A herança múltipla deve ser utilizada criteriosamente porque contém muita complexidade adicional. [Meyers, 1992] fornece uma discussão detalhada sobre as complexidades atribuídas a ambigüidades de nome em potencial e herança repetida. As complexidades surgem de:
Ambigüidades, quando os mesmos nomes forem utilizados por várias classes, qualquer referência não qualificada aos nomes será hereditariamente ambígua. A ambigüidade pode ser resolvida qualificando os nomes do membro com seus nomes de classe. No entanto, isso tem o infeliz efeito de destruir o polimorfismo e transformar as funções virtuais em funções estaticamente vinculadas.
A herança repetida (herança de uma classe-base várias vezes por uma classe derivada através de caminhos diferentes na hierarquia de herança) de vários conjuntos de membros de dados da mesma base gera o problema de quais dos vários conjuntos de membros de dados devem ser utilizados?
Os membros de dados herdados multiplicados podem ser evitados, utilizando a herança virtual (herança das classes-base virtuais). Então porque não utilizar sempre a herança virtual? A herança virtual possui o efeito negativo de alterar a representação de objeto subjacente e reduzir a eficiência de acesso.
Representar uma política que necessite que toda a herança seja virtual, impondo, ao mesmo tempo, todo o espaço ao redor e penalidade de tempo, seria muito autoritário.
Portanto, várias heranças requerem que os designers da classe estejam preparados para os futuros usos de suas classes: para conseguir tomar a decisão para utilizar a herança virtual ou não-virtual.
Esse capítulo fornece orientação sobre os problemas de compilação
Não inclua em uma especificação de módulo outros arquivos de cabeçalho que sejam requeridos apenas pela implementação do módulo.
Evite incluir os arquivos de cabeçalho em uma especificação para o propósito de obter a visibilidade a outras classes, quando for requerida apenas a visibilidade do ponteiro ou da referência; em vez disso, utilize as declarações de avanço.
// Especificação do módulo A, contida no arquivo "A.hh" #include "B.hh" // Don't include when only required by // the implementation. #include "C.hh" // Don't include when only required by // reference; use a forward declaration instead. class C; class A { C* a_c_by_reference; // Has-a by reference. }; // Fim de "A.hh"
Minimizar as dependências de compilação é racional para determinados idiomas de design, diversamente nomeados: Handle ou Envelope [Meyers, 1992] ou classes Bridge [Gamma]. Ao dividir a responsabilidade para uma abstração de classe entre duas classes associadas, uma fornecendo a interface de classe e a outra a implementação; as dependências entre uma classe e seus clientes são minimizadas uma vez que qualquer alteração na implementação (a classe de implementação) não provoca mais a recompilação dos clientes.
// Especificação do módulo A, contida no arquivo "A.hh" class A_implementation; class A { A_implementation* the_implementation; }; // Fim de "A.hh"
Essa abordagem também permite que a classe da interface e a classe de implementação sejam especializadas como duas hierarquias de classe separadas.
NDEBUG
com um valor específicoO símbolo NDEBUG
foi utilizado tradicionalmente para compilar o
código de asserção implementado utilizando a macro de asserção. O paradigma de uso
tradicional era definir o símbolo quando fosse desejado eliminar as asserções;
no entanto, os desenvolvedores geralmente desconheciam a presença das asserções e,
portanto, nunca definiam o símbolo.
Defendemos o uso da versão de gabarito da asserção; nesse caso, o símbolo
NDEBUG
deve receber um valor explícito:
0, se o código de asserção for desejado; não-zero para eliminação. Qualquer código
de asserção subseqüentemente compilado sem fornecer ao símbolo NDEBUG
um valor específico gerará erros de compilação; portanto, trazendo a atenção dos
desenvolvedores à existência do código de asserção.
Segue um resumo de todas as diretrizes apresentadas nesse folheto.
Utilize o Senso Comum
Sempre utilize #include
para obter acesso a
uma especificação de módulo
Nunca declare nomes que comecem com um ou mais sublinhados ('_')
Limite as declarações globais apenas aos espaços de nomes
Sempre forneça um construtor padrão para as classes com construtores explicitamente declarados
Sempre declare os construtores de cópia e os operadores de
designação para as classes com os membros de dados do tipo de ponteiro
Nunca declare novamente os parâmetros do construtor para ter um valor padrão
Sempre declare os destruidores como virtuais
Nunca redefina as funções não virtuais
Nunca chame as funções de membro a partir de um inicializador construtor
Nunca retorne uma referência a um objeto local
Nunca retorne um ponteiro de-referenciado inicializado com new
Nunca retorne uma referência non-const ou ponteiro para os dados do membro
Faça com que operator=
retorne uma referência a *this
Faça com que operator=
verifique a auto-designação
Nunca lance "constness" de um objeto constante
Não assuma qualquer ordem de avaliação de expressão específica
Não utilize o lançamento de estilo antigo
Utilize o novo tipo bool
para as expressões
Booleanas
Nunca compare diretamente com o valor Booleano verdadeiro
Nunca compare os ponteiros aos objetos que não estão dentro
da mesma matriz
Sempre designe um valor de ponteiro null
a um ponteiro de objeto excluído
Sempre forneça uma ramificação padrão para switch-statements para capturar os erros
Não utilize goto-statement
Evite misturar as operações de memória C e C++
Sempre utilize delete[] ao excluir os objetos de matriz criados por new
Nunca utilize os nomes de caminho de arquivo de hardcode atribuído
Não assuma a representação de um tipo
Não assuma o alinhamento de um tipo
Não dependa de um determinado comportamento de limite baixo
ou estouro
Não converta de um tipo "mais curto" para um
"mais extenso"
Defina o símbolo NDEBUG
com um valor específico
Coloque as especificações de módulo e as implementações em arquivos separados
Selecione um único conjunto de extensões de nome de arquivo
para distinguir os cabeçalhos dos arquivos de implementação
Evite definir mais de uma classe por especificação do módulo
Evite colocar as declarações de implementação privada nas especificações do módulo
Coloque as definições de função seqüencial do módulo em um arquivo separado
Quebre grandes módulos em várias unidades de conversão, se o
tamanho do programa for uma preocupação
Isole as dependências de plataforma
Proteja-se das inclusões repetidas de arquivo
Utilize um símbolo de compilação condicional
"No_Inline
" para contestar a compilação seqüencial
Utilize um estilo de recuo pequeno consistente para instruções aninhadas
Recue os parâmetros de função a partir do nome da função ou nome do escopo
Utilize um comprimento máximo de linha que caberia no tamanho do papel impresso padrão
Utilize a dobra de linha consistente
Utilize os comentários de estilo C++ em vez de comentários de estilo C
Maximize a proximidade do comentário para o código fonte
Evite os comentários de fim de linha
Evite os cabeçalhos de comentário
Utilize uma linha de comentário vazia para separar os parágrafos de comentário
Evite a redundância
Escreva o código de auto-documentação em vez dos comentários
Documente as classes e as funções
Escolha uma convenção de nomenclatura e aplique-a consistentemente
Evite utilizar nomes de tipo que sejam diferentes apenas por tipo de letra
Evite o uso de abreviações
Evite o uso de sufixos para denotar as construções de idioma
Escolha nomes limpos, legíveis e significativos
Utilize a ortografia correta nos nomes
Utilize as cláusulas de predicado positivo para Booleanos
Utilize os espaços de nome para particionar os nomes globais
em potencial por subsistemas ou por bibliotecas
Utilize substantivos ou frases substantivas para os nomes de classe
Utilize os verbos para nomes de função de tipo de procedimento
Utilize a sobrecarga de função quando for pretendido o mesmo significado geral
Aumente os nomes dos elementos gramaticais para enfatizar o significado
Escolha nomes de exceção com um significado negativo
Utilize os adjetivos definidos pelo projeto para os nomes de exceção
Utilize primeiras letras maiúsculas para o exponente de
ponto flutuante e dígitos hexadecimais.
Utilize um espaço de nomes para agrupar a funcionalidade não-classe
Minimize o uso dos dados de escopo global e do espaço de nomes
Utilize a classe em vez de struct para implementar os tipos de dados abstratos
Declare os membros de classe na ordem de acessibilidade decrescente
Evite declarar membros de dados públicos ou protegidos para tipos de dados abstratos
Utilize os atalhos para preservar o encapsulamento
Evite fornecer definições de função nas declarações da classe
Evite declarar vários operadores de conversão
e construtores únicos de parâmetro
Utilize as funções não-virtuais criteriosamente
Utilize os inicializadores construtores em vez das designações nos construtores
Cuidado ao chamar as funções de membro nos construtores e destruidores
Utilize static const para constantes de classe integral
Sempre declare um tipo de retorno de função explícita
Sempre forneça nomes de parâmetros formais nas declarações de função
Esforce-se para funções com um único ponto de retorno
Evite criar função com efeitos colaterais globais
Declare os parâmetros de função na ordem de volatilidade e importância decrescente
Evite declarar funções com um número variável de parâmetros
Evite declarar novamente as funções com parâmetros padrão
Maximize o uso de constantes nas declarações de função
Evite transmitir os objetos por valor
Utilize as funções seqüenciais em preferência a
#define
para a expansão da macro
Utilize os parâmetros padrão em vez da sobrecarga de função
Utilize a sobrecarga de função para expressar as semânticas comuns
Evite sobrecarregar as funções que possuem ponteiros e
inteiros
Minimize a complexidade
Evite o uso de tipos fundamentais
Evite utilizar valores literais
Evite utilizar a diretiva #define
do pré-processador para definir as constantes
Declare os objetos próximos a seus pontos de primeiro uso
Sempre inicialize os objetos const na declaração
Inicialize os objetos na definição
Utilize uma if-statement ao ramificar expressões Booleanas
Utilize uma switch-statement ao ramificar valores
distintos
Utilize um for-statement ou while-statement quando um teste
pré-iteração for requerido em um loop
Utilize um do-while-statement quando um teste pós-iteração for requerido em um loop
Evite o uso das instruções jump nos loops
Evite o ocultamento dos identificadores nos escopos aninhados
Utilize as asserções de forma liberal durante o desenvolvimento para detectar os erros
Utilize as exceções apenas para condições realmente excepcionais
Derive as exceções de projeto a partir das exceções padrão
Minimize o número de exceções utilizadas por uma determinada abstração
Declare todas as exceções emitidas
Defina as rotinas de tratamento de exceção na ordem de
classe mais derivada para a de maior base
Evite as rotinas de tratamento de exceção catch-all
Certifique-se de que os códigos de status da função possuam um valor apropriado
Execute as verificações de segurança localmente; não espere que seu cliente faça isso
Utilize as constantes "stretchable" sempre que possível
Utilize os componentes da biblioteca padrão sempre que possível
Defina os tipos de sistema globais na amplitude do projeto
Utilize typedef para criar sinônimos para reforçar o significado local
Utilize os parênteses redundantes para esclarecer as expressões compostas
Evite aninhar as expressões muito profundamente
Utilize 0 para ponteiros nulos em vez de NULL
Relate as exceções na primeira ocorrência