Então notei que é possível evitar colocar funções privadas nos cabeçalhos, fazendo algo assim:
// In file pred_list.h:
class PredicateList
{
int somePrivateField;
friend class PredicateList_HelperFunctions;
public:
bool match();
}
// In file pred_list.cpp:
class PredicateList_HelperFunctions
{
static bool fullMatch(PredicateList& p)
{
return p.somePrivateField == 5; // or whatever
}
}
bool PredicateList::match()
{
return PredicateList_HelperFunctions::fullMatch(*this);
}
A função privada nunca é declarada no cabeçalho, e os consumidores da classe que importam o cabeçalho nem precisam saber que ela existe. Isso é necessário se a função auxiliar for um modelo (a alternativa é colocar o código completo no cabeçalho), e foi assim que "descobri" isso. Outra boa vantagem de não precisar recompilar todos os arquivos que incluem o cabeçalho se você adicionar / remover / modificar uma função de membro privada. Todas as funções privadas estão no arquivo .cpp.
Então...
- Esse é um padrão de design conhecido para o qual existe um nome?
- Para mim (vindo de um background Java / C # e aprendendo C ++ no meu próprio tempo), isso parece uma coisa muito boa, pois o cabeçalho está definindo uma interface, enquanto o .cpp está definindo uma implementação (e o tempo de compilação aprimorado é um bom bônus). No entanto, também cheira a abusar de um recurso de idioma que não se destina a ser usado dessa maneira. Então, qual é? Isso é algo que você desaprovaria ver em um projeto profissional de C ++?
- Alguma armadilha em que não estou pensando?
Estou ciente do Pimpl, que é uma maneira muito mais robusta de ocultar a implementação na borda da biblioteca. Isso é mais para uso com classes internas, onde o Pimpl causaria problemas de desempenho ou não funcionaria porque a classe precisa ser tratada como um valor.
EDIT 2: A excelente resposta da Dragon Energy abaixo sugeriu a seguinte solução, que não usa a friend
palavra-chave:
// In file pred_list.h:
class PredicateList
{
int somePrivateField;
class Private;
public:
bool match();
}
// In file pred_list.cpp:
class PredicateList::Private
{
public:
static bool fullMatch(PredicateList& p)
{
return p.somePrivateField == 5; // or whatever
}
}
bool PredicateList::match()
{
return PredicateList::Private::fullMatch(*this);
}
Isso evita o fator de choque de friend
(que parece ter sido demonizado goto
), mantendo o mesmo princípio de separação.
fonte
Respostas:
É um pouco esotérico, para dizer o mínimo, como você já reconheceu, o que pode me fazer coçar a cabeça por um momento, quando começo a encontrar seu código, imaginando o que você está fazendo e onde essas classes auxiliares são implementadas até que eu comece a escolher seu estilo / hábitos (em que ponto eu posso me acostumar totalmente).
Eu gosto que você esteja reduzindo a quantidade de informações nos cabeçalhos. Especialmente em bases de código muito grandes, que podem ter efeitos práticos para reduzir dependências em tempo de compilação e, finalmente, criar tempos.
Minha reação instintiva é que, se você sentir necessidade de ocultar os detalhes da implementação dessa maneira, favorecer a passagem de parâmetros para funções independentes com ligação interna no arquivo de origem. Geralmente, você pode implementar funções utilitárias (ou classes inteiras) úteis para implementar uma classe específica sem ter acesso a todos os elementos internos da classe e apenas passar os relevantes da implementação de um método para a função (ou construtor). E, naturalmente, isso tem o bônus de reduzir o acoplamento entre sua classe e os "ajudantes". Ele também tende a generalizar o que de outra forma poderia ter sido "ajudante" se você descobrir que eles estão começando a servir a um propósito mais generalizado aplicável a mais de uma implementação de classe.
Se isso se tornar pesado, consideraria uma segunda solução mais idiomática que é a pimpl (eu sei que você mencionou problemas com ela, mas acho que você pode generalizar uma solução para evitar aqueles com o mínimo esforço). Isso pode mover um monte de informações que sua classe precisa ser implementada, incluindo seus dados privados, fora do cabeçalho do atacado. Os problemas de desempenho do pimpl podem ser atenuados em grande parte com um alocador de tempo constante e barato * como uma lista gratuita, preservando a semântica de valores sem a necessidade de implementar um copiador completo e definido pelo usuário.
Pessoalmente, somente depois de esgotar essas possibilidades eu consideraria algo assim. Eu acho que é uma idéia decente se a alternativa é como métodos mais particulares expostos ao cabeçalho, talvez apenas com a natureza esotérica dele ser a preocupação prática.
Uma alternativa
Uma alternativa que me veio à cabeça agora e que cumpre em grande parte seus mesmos propósitos amigos ausentes é a seguinte:
Agora isso pode parecer uma diferença muito discutível e eu ainda chamaria isso de "ajudante" (em um sentido possivelmente depreciativo, pois ainda estamos passando o estado interno inteiro da classe para a função, seja ela necessária ou não) exceto que evita o fator "choque" de encontrar
friend
. Em geral,friend
parece um pouco assustador ver freqüentemente uma inspeção adicional ausente, pois ela diz que os internos de sua classe estão acessíveis em outros lugares (o que implica que ela pode ser incapaz de manter seus próprios invariantes). Com o modo como você está usandofriend
, torna-se bastante discutível se as pessoas estão cientes da prática desde que ofriend
está apenas residindo no mesmo arquivo de origem, ajudando a implementar a funcionalidade privada da classe, mas o acima mencionado realiza o mesmo efeito, pelo menos com o benefício possivelmente discutível de não envolver nenhum amigo que evite todo esse tipo ("Oh!" atirar, essa turma tem um amigo. Onde mais seus particulares são acessados / alterados? "). Enquanto a versão imediatamente acima comunica imediatamente que não há como os privados serem acessados / alterados fora de qualquer coisa feita na implementação doPredicateList
.Talvez isso esteja se movendo em direção a territórios um tanto dogmáticos com esse nível de nuance, já que qualquer um pode descobrir rapidamente se você nomeia as coisas uniformemente
*Helper*
e as coloca no mesmo arquivo de origem que é todo agrupado como parte da implementação privada de uma classe. Mas, se ficarmos exigentes, talvez o estilo imediatamente acima não cause uma reação instantânea sem afriend
palavra - chave que tende a parecer um pouco assustadora.Para as outras perguntas:
Essa pode ser uma possibilidade através dos limites da API em que o cliente pode definir uma segunda classe com o mesmo nome e obter acesso aos internos dessa maneira, sem erros de ligação. Por outro lado, sou largamente um codificador C que trabalha com gráficos, onde as preocupações com segurança nesse nível de "e se" são muito baixas na lista de prioridades, então preocupações como essas são apenas aquelas em que costumo acenar com as mãos e fazer uma dança e tente fingir que eles não existem. :-D Se você estiver trabalhando em um domínio em que preocupações como essas são bastante sérias, acho que é uma consideração decente a ser feita.
A proposta alternativa acima também evita sofrer esse problema. Se você ainda deseja continuar usando
friend
, você também pode evitar esse problema, tornando o auxiliar uma classe aninhada privada.Nenhum que eu saiba. Eu meio que duvido que houvesse um, já que ele realmente está entrando nas minúcias dos detalhes e estilo da implementação.
"Inferno do ajudante"
Eu recebi um pedido de esclarecimentos adicionais sobre como às vezes me encolho quando vejo implementações com muito código "auxiliar", e isso pode ser um pouco controverso com alguns, mas é realmente factual, como eu realmente encolhi quando estava depurando alguns da implementação de uma classe por meus colegas apenas para encontrar muitos "ajudantes". :-D E eu não era o único na equipe coçando a cabeça tentando descobrir o que todos esses ajudantes deveriam fazer exatamente. Eu também não quero parecer dogmático como "Você não usará ajudantes", mas faria uma pequena sugestão de que poderia ajudar a pensar em como implementar coisas ausentes deles quando praticável.
E sim, estou incluindo métodos privados. Se eu vejo uma classe com uma interface pública simples, mas com um conjunto interminável de métodos privados que são um pouco mal definidos em propósitos como
find_impl
oufind_detail
oufind_helper
, então também me encolho de maneira semelhante.O que estou sugerindo como alternativa são funções de não-membro não-membro com vínculo interno (declarado
static
ou dentro de um espaço de nome anônimo) para ajudar a implementar sua classe com pelo menos um propósito mais generalizado do que "uma função que ajude a implementar outras". E posso citar Herb Sutter de C ++ 'Coding Standards' aqui, por que isso pode ser preferível do ponto de vista geral do SE:Você também pode entender as "taxas de associação" sobre as quais ele fala em termos do princípio básico de restringir o escopo variável. Se você imaginar, como o exemplo mais extremo, um objeto Deus com todo o código necessário para a execução de todo o programa, favorecendo "ajudantes" desse tipo (funções, sejam membros ou amigos) que possam acessar todos os internos ( privados) de uma classe basicamente tornam essas variáveis não menos problemáticas que as variáveis globais. Você tem todas as dificuldades de gerenciar o estado corretamente e encadear a segurança e manter invariantes que obteria com variáveis globais neste exemplo mais extremo. E é claro que a maioria dos exemplos reais não chega nem perto desse extremo, mas a ocultação de informações é tão útil quanto limita o escopo das informações acessadas.
Agora, Sutter já oferece uma boa explicação aqui, mas eu acrescentaria ainda que a dissociação tende a promover como uma melhoria psicológica (pelo menos se seu cérebro funcionar como o meu) em termos de como você projeta as funções. Quando você começa a projetar funções que não podem acessar tudo na classe, exceto apenas os parâmetros relevantes que você passa ou, se você passar a instância da classe como parâmetro, apenas seus membros públicos, ela tende a promover uma mentalidade de design que favorece funções que têm um propósito mais claro, além da dissociação e promovem o encapsulamento aprimorado, do que o que você poderia tentar criar se pudesse acessar tudo.
Se voltarmos às extremidades, uma base de código repleta de variáveis globais não tenta exatamente os desenvolvedores a projetar funções de uma maneira clara e generalizada. Muito rapidamente, quanto mais informações você pode acessar em uma função, mais muitos de nós, mortais, enfrentamos a tentação de degeneralizá-la e reduzir sua clareza em favor de acessar todas essas informações extras que temos, em vez de aceitar parâmetros mais específicos e relevantes para essa função restringir seu acesso ao estado e ampliar sua aplicabilidade e melhorar sua clareza de intenções. Isso se aplica (embora geralmente em menor grau) a funções de membros ou amigos.
fonte
PredicateList
, geralmente pode ser possível passar apenas um membro ou dois da lista de predicados para uma função um pouco mais generalizada que não precisa de acesso a todos os membros privados dePredicateList
, e freqüentemente isso, também tenderão a fornecer um nome e um propósito mais claro e generalizado para essa função interna, além de mais oportunidades de "reutilização de código em retrospectiva".