Guia de estilo C ++ do Google. Parte 4

Parte 1. Introdução

...

Parte 4. Aulas

...





Este artigo é uma tradução de parte do guia de estilo C ++ do Google para o russo.

Artigo original (fork no github), tradução atualizada .





Aulas





As classes são o bloco de construção principal em C ++. E, claro, eles são usados ​​com frequência. Esta seção descreve as regras básicas e proibições a serem seguidas ao usar classes.



Código no construtor





Não chame métodos virtuais no construtor. Evite inicialização que pode falhar (e não há maneira de sinalizar um erro. Observação: observe que o Google não gosta de exceções).



Definição



Em geral, qualquer inicialização pode ser realizada em um construtor (ou seja, toda inicialização pode ser feita em um construtor).



Por



  • Não há necessidade de se preocupar com a classe não inicializada.
  • Os objetos que são totalmente inicializados no construtor podem ser const e também são mais fáceis de usar em contêineres e algoritmos padrão.




Vs



  • Se as funções virtuais forem chamadas no construtor, as implementações da classe derivada não serão chamadas. Mesmo que a classe agora não tenha descendentes, isso pode se tornar um problema no futuro.
  • ( ) ( ).
  • , ( — ) . : bool IsValid(). .
  • . , , .




Veredito Os



construtores não devem chamar funções virtuais. Em alguns casos (se permitido), os erros de projeto podem ser tratados até o encerramento do programa. Caso contrário, considere o padrão Factory Method ou use Init () (mais detalhes aqui: TotW # 42 ). Use Init () apenas se o objeto tiver sinalizadores de estado que permitem chamar certas funções públicas (uma vez que é difícil trabalhar totalmente com um objeto parcialmente construído).



Conversões implícitas





Não declare conversões implícitas. Use a palavra-chave explícita para operadores de conversão de tipo e construtores de argumento único.



Definição As



conversões implícitas permitem que um objeto de um tipo de origem seja usado onde outro tipo (tipo de destino) é esperado, como passar um argumento do tipo int para uma função que espera um duplo .



Além das conversões implícitas especificadas pela linguagem de programação, você também pode definir suas próprias conversões personalizadas adicionando os membros apropriados à declaração da classe (origem e destino). A conversão implícita do lado da fonte é declarada como tipo operador + receptor (por exemplo, operador bool () ). A conversão implícita no lado do receptor é implementada por um construtor que leva o tipo de origem como o único argumento (além dos argumentos padrão).



A palavra-chave explícita pode ser aplicada a um construtor ou operador de conversão para indicar explicitamente que uma função só pode ser usada quando há uma correspondência de tipo explícita (por exemplo, após uma operação de conversão). Isso se aplica não apenas a conversões implícitas, mas também a listas de inicialização em C ++ 11:



class Foo {
  explicit Foo(int x, double y);
  ...
};
void Func(Foo f);

      
      







Func({42, 3.14});  // 

      
      







Este exemplo de código não é tecnicamente uma conversão implícita, mas a linguagem o trata como se fosse explícito .



Por



  • , .
  • , string_view std::string const char*.
  • .








  • , ( ).
  • , : , .
  • , .
  • explicit : , .
  • , . — , .
  • , , , .








Operadores de conversão de veredito e construtores de argumento único devem ser declarados com a palavra-chave explícita . Também há uma exceção: os construtores de cópia e movimentação podem ser declarados sem explícito , uma vez que eles não realizam conversão de tipo. Além disso, conversões implícitas podem ser necessárias no caso de classes de wrapper para outros tipos (neste caso, certifique-se de pedir permissão ao seu gerenciamento superior para ignorar essa regra importante).



Construtores que não podem ser chamados com um único argumento podem ser declarados sem explícito . Construtores aceitando um único std :: initializer_listtambém deve ser declarado sem explícito para suportar a inicialização de cópia (por exemplo, MyType m = {1, 2}; ).



Tipos copiáveis ​​e relocáveis





A interface pública da classe deve indicar explicitamente a capacidade de copiar e / ou mover, ou vice-versa, proibir tudo. Ofereça suporte à cópia e / ou movimentação apenas se essas operações fizerem sentido para o seu tipo.



Definição Um



tipo relocável é aquele que pode ser inicializado ou atribuído a partir de valores temporários.



Tipo copiável - pode ser inicializado ou atribuído a partir de outro objeto do mesmo tipo (ou seja, o mesmo que o realocável), desde que o objeto original permaneça inalterado. Por exemplo , std :: unique_ptr <int> é realocável, mas não o tipo a ser copiado (porque o valor do objeto de origem std :: unique_ptr <int> deve mudar quando atribuído ao objeto de destino). inte std :: string são exemplos de tipos relocáveis ​​que também podem ser copiados: para int as operações de mover e copiar são as mesmas, para std :: string a operação de mover requer menos recursos do que copiar.



Para tipos definidos pelo usuário, a cópia é especificada pelo construtor de cópia e o operador de cópia.O movimento é especificado pelo construtor de movimento com o operador de movimento ou (se não estiver presente) pelas funções de cópia correspondentes.



Os construtores de cópia e movimentação podem ser chamados implicitamente pelo compilador, por exemplo, ao passar objetos por valor.



Por



Objetos de tipos copiáveis ​​e relocáveis ​​podem ser passados ​​e recebidos por valor, o que torna a API mais simples, segura e versátil. Neste caso, não há problemas com a propriedade do objeto, seu ciclo de vida, mudança de valor, etc., e também não é necessário especificá-los no "contrato" (tudo isso é diferente de passar objetos por ponteiro ou referência). A comunicação lenta entre o cliente e a implementação também é evitada, tornando o código muito mais fácil de entender, manter e otimizar para o compilador. Esses objetos podem ser usados ​​como argumentos para outras classes que requerem passagem por valor (por exemplo, a maioria dos contêineres) e, em geral, são mais flexíveis (por exemplo, quando usados ​​em padrões de projeto).



Construtores de copiar / mover e operadores de atribuição associados são geralmente mais fáceis de definir do que alternativas como Clone () , CopyFrom () ou Swap () porque o compilador pode gerar as funções necessárias (implicitamente ou com = default ). Eles (funções) são fáceis de declarar e você pode ter certeza de que todos os alunos serão copiados. Construtores (copiar e mover) são geralmente mais eficientes porque não requerem alocação de memória, inicialização separada, atribuições adicionais, são bem otimizados (veja elisão de cópia ).



Os operadores de movimentação permitem que você manipule de forma eficiente (e implícita) os recursos rvalue dos objetos. Isso às vezes torna a codificação mais fácil.



Contra



Alguns tipos não precisam ser copiáveis ​​e o suporte para operações de cópia pode ser contra-intuitivo ou causar operação incorreta. Tipos de singletones ( Registrador ), objetos para limpeza (por exemplo, quando sair do escopo) ( Limpeza ) ou contendo dados exclusivos ( Mutex ) são, em seu significado, não copiáveis. Além disso, as operações de cópia para classes básicas que têm descendentes podem levar ao fracionamento do objeto.... As operações de cópia padrão (ou mal escritas) podem levar a erros que são difíceis de detectar.



Construtores de cópia são chamados implicitamente e isso é fácil de ignorar (especialmente para programadores que escreveram anteriormente em linguagens onde os objetos são passados ​​por referência). Você também pode reduzir o desempenho fazendo cópias desnecessárias.



Veredicto A



interface pública de cada classe deve indicar explicitamente quais operações de cópia e / ou movimentação ela suporta. Isso geralmente é feito na seção pública na forma de declarações explícitas das funções necessárias ou declarando-as como exclusão.



Em particular, a classe copiada deve declarar explicitamente as operações de cópia; apenas uma classe relocável deve declarar explicitamente as operações de movimentação; uma classe não copiável / não movível deve negar explicitamente ( = excluir ) as operações de cópia. Declarar ou excluir explicitamente todas as quatro funções de copiar e mover também é permitido, embora não seja obrigatório. Se você implementar o operador de copiar e / ou mover, você também deve fazer o construtor correspondente.



class Copyable {
 public:
  Copyable(const Copyable& other) = default;
  Copyable& operator=(const Copyable& other) = default;
  //       (..  )
};
class MoveOnly {
 public:
  MoveOnly(MoveOnly&& other);
  MoveOnly& operator=(MoveOnly&& other);
  //     .  ( )    :
  MoveOnly(const MoveOnly&) = delete;
  MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
 public:
  //       
  NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
  NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
      = delete;
  //     (),    :
  NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
  NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
      = delete;
};

      
      







As declarações ou exclusões de funções descritas podem ser omitidas em casos óbvios:



  • Se a classe não contiver uma seção privada (por exemplo, uma estrutura ou uma classe de interface), a capacidade de cópia e realocação podem ser declaradas por meio de uma propriedade semelhante de qualquer membro público.
  • , . , , .
  • , () /, / (.. ). / . .




Um tipo não deve ser declarado copiável / relocável a menos que o programador comum entenda a necessidade dessas operações, ou se as operações exigirem muitos recursos e desempenho. As operações de movimentação para tipos copiados são sempre uma otimização de desempenho, mas, por outro lado, são uma fonte potencial de bugs e complicações. Portanto, não declare as operações de movimentação, a menos que elas forneçam ganhos de desempenho significativos sobre a cópia. Em geral, é desejável (se as operações de cópia forem declaradas para uma classe) projetar tudo de forma que as funções de cópia padrão sejam usadas. E certifique-se de verificar a exatidão de todas as operações por padrão.



Por causa do risco de "fracionamento", é preferível evitar copiar e mover as instruções públicas para classes que você planeja usar como classes base (e preferencialmente não herdar de uma classe com tais funções). Se você precisar tornar a classe base copiável, crie uma função virtual pública Clone () e um construtor de cópia protegida para que a classe derivada possa usá-los para implementar operações de cópia.



Estruturas vs Classes





Use structs apenas para objetos passivos que armazenam dados. Em outros casos, use as classes ( classe ).



As palavras-chave struct e class são quase idênticas em C ++. No entanto, temos nossa própria compreensão de cada palavra-chave, portanto, use aquela que se adequar ao seu propósito e significado.



As estruturas devem ser usadas para objetos passivos, apenas para transferência de dados. Eles podem ter constantes próprias, mas não deve haver nenhuma funcionalidade (com a possível exceção das funções get / set). Todos os campos devem ser públicos, disponíveis para acesso direto, e isso é preferível ao uso das funções get / set. As estruturas não devem conter invariantes (por exemplo, valores calculados) que são baseados em dependências entre diferentes campos da estrutura: a capacidade de modificar diretamente os campos pode invalidar o invariante. Os métodos não devem restringir o uso da estrutura, mas podem atribuir valores aos campos: ou seja, como um construtor, destruidor ou funções Initialize () , Reset () .



Se for necessária funcionalidade adicional no processamento de dados ou invariantes, é preferível usar as classes ( classe ). Além disso, em caso de dúvida sobre qual escolher - use classes.



Em alguns casos ( meta-funções de modelo , características, alguns functores) para consistência com o STL, é permitido usar estruturas em vez de classes.



Lembre-se de que as variáveis ​​em estruturas e classes são nomeadas em estilos diferentes.



Estruturas vs pares e tuplas





Se os elementos individuais em um bloco de dados puderem ser nomeados de maneira significativa, é desejável usar estruturas em vez de pares ou tuplas.



Enquanto usando pares e tuplas evita reinventar a roda com seu próprio tipo e você vai economizar muito tempo escrevendo código, campos com nomes significativos (em vez de .Em primeiro lugar , .segunda, ou std :: get <X> ) será mais fácil de ler quando a leitura do código. E embora C ++ 14 adicione acesso de tipo ( std :: get <Type> , e o tipo deve ser único) além de acesso de índice para tuplas , o nome do campo é muito mais informativo do que o tipo.



Pares e tuplas são apropriados em código onde não há distinção especial entre os elementos de um par ou tupla. Eles também precisam trabalhar com códigos ou APIs existentes.



Herança





A composição da classe geralmente é mais apropriada do que a herança. Ao usar herança, torne-a pública .



Definição



Quando uma classe filha herda de uma classe base, ela inclui as definições de todos os dados e operações da base. A herança da interface é a herança de uma classe base abstrata pura (nenhum estado ou método é definido nela). Todo o resto é "herança de implementação".



Por



A herança de implementação reduz o tamanho do código, reutilizando partes da classe base (que se torna parte da nova classe). Porque herança é uma declaração em tempo de compilação que permite ao compilador entender a estrutura e encontrar erros. A herança da interface pode ser usada para fazer a classe suportar a API necessária. Além disso, o compilador pode encontrar erros se a classe não definir o método necessário da API herdada.



Contras



No caso de herança de implementação, o código começa a se confundir entre as classes base e filha e isso pode complicar a compreensão do código. Além disso, a classe filha não pode substituir o código de funções não virtuais (não pode alterar sua implementação).



A herança múltipla é ainda mais problemática e às vezes leva à degradação do desempenho. Freqüentemente, a penalidade de desempenho ao mudar de herança única para herança múltipla pode ser maior do que a transição de funções regulares para funções virtuais. Além disso, há uma etapa da herança múltipla para a rômbica, e isso já leva à ambigüidade, confusão e, é claro, a bugs.



Veredicto



Qualquer herança deve ser pública . Se você deseja torná-lo privado , é melhor adicionar um novo membro com uma instância da classe base.



Não abuse da herança de implementação. A composição da classe é freqüentemente preferida. Tente limitar o uso da semântica de herança "é»: Bar , você pode herdar do Foo , se posso dizer que o Bar "é» o Foo (ou seja, quando usado o Foo , você também pode usar o Bar ).



Protected ( protected, ) faz apenas aquelas funções que deveriam estar disponíveis para as classes filhas. Observe que os dados devem ser privados.



Declare explicitamente as substituições de função / destruidor virtual usando especificadores: ou substitua ou (se necessário) final . Não use o especificador virtual ao substituir funções. Explicação: Uma função ou destruidor que está marcado como substituição ou final, mas não é virtual, simplesmente não será compilado (o que ajuda a detectar erros comuns). Além disso, os especificadores funcionam como documentação; e se não houver especificadores, o programador será forçado a verificar toda a hierarquia para esclarecer a virtualidade da função.



A herança múltipla é permitida, no entanto a herança múltipla implementação não é recomendada a partir da palavra.



Sobrecarga do operador





Sobrecarregue os operadores o mais razoavelmente possível. Não use literais personalizados.



A determinação do



código C ++ permite que o usuário substitua os operadores integrados usando o operador de palavra-chave e o tipo de usuário como um dos parâmetros; também o operador permite definir novos literais usando o operador "" ; você também pode criar funções de conversão como operador bool () .



Por



Usar a sobrecarga de operador para tipos definidos pelo usuário (semelhantes aos tipos integrados) pode tornar seu código mais conciso e intuitivo. Operadores sobrecarregados correspondem a certas operações (por exemplo, == , < , = e << ) e se o código seguir a lógica de aplicação dessas operações, os tipos definidos pelo usuário podem ser mais claros e usados ​​ao trabalhar com bibliotecas externas que dependem dessas operações.



Literais personalizados são uma forma muito eficiente de criar objetos personalizados.



Vs



  • (, ) — , , .
  • , .
  • , , .
  • , , .
  • , .
  • / ( ), «» . , foo < bar &foo < &bar; .
  • . & , . &&, || , () ( ) .
  • , . , .
  • (UDL) , C++ . : «Hello World»sv std::string_view(«Hello World»). , .
  • Porque nenhum namespace é especificado para o UDL, você precisará usar uma diretiva using (que é proibida ) ou uma declaração using (que também é proibida (em arquivos de cabeçalho) , a menos que os nomes importados façam parte da interface mostrada no arquivo de cabeçalho). Para esses arquivos de cabeçalho, é melhor evitar sufixos UDL e é desejável evitar dependências entre literais que são diferentes no arquivo de cabeçalho e de origem.




Veredicto



Defina operadores sobrecarregados apenas se seu significado for óbvio, claro e consistente com a lógica geral. Por exemplo, use | no sentido da operação OR; implementar lógica de tubulação em vez disso não é uma boa ideia.



Defina operadores apenas para seus próprios tipos, faça-o no mesmo cabeçalho e arquivo de origem e no mesmo namespace. Como resultado, os operadores estarão disponíveis no mesmo local que os próprios tipos, e o risco de múltiplas definições é mínimo. Sempre que possível, evite definir operadores como modelos. você deve corresponder a qualquer conjunto de argumentos do modelo. Se você definir um operador, defina também "irmãos" para ele. E tome cuidado com a consistência dos resultados que eles retornam. Por exemplo, se você definir um operador < , defina todos os operadores de comparação e certifique-se de que os operadores < e > nunca retornem verdadeiro para os mesmos argumentos.



É desejável definir operadores binários imutáveis ​​como funções externas (não membros). Se o operador binário for declarado membro da classe, a conversão implícita pode ser aplicada ao argumento direito, mas não ao esquerdo. Isso pode ser um pouco frustrante para os programadores se (por exemplo) o código a <b compilar, mas b <a não.



Não há necessidade de tentar ignorar as substituições do operador. Se a comparação (ou atribuição e função de saída) for necessária, é melhor definir == (ou = e << ) em vez de Equals () , CopyFrom () e PrintTo () . Por outro lado, você não precisa redefinir um operador apenas porque as bibliotecas externas o esperam. Por exemplo, se o tipo de dados não pode ser ordenado e você deseja armazená-lo em std :: set , então é melhor fazer uma função de comparação personalizada e não usar o operador < .



Não substitua && , || , , (Vírgula) ou unário & . Não substitua o operador "" , ou seja, não adicione seus próprios literais. Não use literais previamente definidos (incluindo a biblioteca padrão e além).



Informações adicionais: a

conversão de tipo é descrita na seção sobre conversões implícitas . O operador = é escrito no construtor de cópia . O tópico de sobrecarga << para trabalhar com fluxos é abordado em fluxos . Você também pode se familiarizar com as regras da seção sobre sobrecarga de funções , que também são adequadas para operadores.



Acessando membros da classe





Sempre torne os dados da classe privados , exceto para constantes . Isso simplifica o uso de invariantes, adicionando funções de acesso mais simples (geralmente constantes).



É permitido declarar os dados da classe como protegidos para uso em classes de teste (por exemplo, ao usar o Google Test ) ou outros casos semelhantes.



Procedimento de anúncio





Coloque anúncios semelhantes em um só lugar, exiba partes comuns.



A definição de classe geralmente começa com uma seção do público: , vai mais longe protegida: e, em seguida, o privado: . Não especifique seções vazias.



Dentro de cada seção, agrupe declarações semelhantes. A ordem preferida é de tipos (incluindo typedef , using , classes e estruturas aninhadas), constantes, métodos de fábrica, construtores, operadores de atribuição, destruidores, outros métodos, membros de dados.



Não coloque definições de método pesadas na definição de classe. Normalmente, apenas métodos triviais, muito curtos ou de desempenho crítico são "embutidos" na definição da classe. Consulte também Funções embutidas .



All Articles