Um dos problemas do pimpl é a penalidade de desempenho em usá-lo (alocação de memória adicional, membros de dados não contíguos, indiretos adicionais, etc.). Gostaria de propor uma variação no idioma pimpl que evite essas penalidades de desempenho às custas de não obter todos os benefícios do pimpl. A idéia é deixar todos os membros de dados privados na própria classe e mover apenas os métodos privados para a classe pimpl. O benefício comparado ao pimpl básico é que a memória permanece contígua (sem necessidade de direcionamento adicional). Os benefícios comparados a não usar pimpl são:
- Esconde as funções privadas.
- Você pode estruturá-lo para que todas essas funções tenham ligação interna e permitam ao compilador otimizá-lo mais agressivamente.
Então, minha idéia é fazer com que o pimpl seja herdado da própria classe (parece um pouco louco, eu sei, mas tenha paciência comigo). Seria algo como isto:
No arquivo Ah:
class A
{
A();
void DoSomething();
protected: //All private stuff have to be protected now
int mData1;
int mData2;
//Not even a mention of a PImpl in the header file :)
};
No arquivo A.cpp:
#define PCALL (static_cast<PImpl*>(this))
namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
static_assert(sizeof(PImpl) == sizeof(A),
"Adding data members to PImpl - not allowed!");
void DoSomething1();
void DoSomething2();
//No data members, just functions!
};
void PImpl::DoSomething1()
{
mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
DoSomething2();
}
void PImpl::DoSomething2()
{
mData2 = baz();
}
}
A::A(){}
void A::DoSomething()
{
mData2 = foo();
PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}
Até onde eu vejo, não há absolutamente nenhuma penalidade de desempenho no uso deste vs pimpl e alguns possíveis ganhos de desempenho e uma interface de arquivo de cabeçalho mais limpa. Uma desvantagem que isso tem em relação ao pimpl padrão é que você não pode ocultar os membros dos dados, portanto as alterações nesses membros ainda acionarão uma recompilação de tudo o que depende do arquivo de cabeçalho. Mas, do meu ponto de vista, é esse benefício ou o desempenho de manter os membros contíguos na memória (ou fazer isso- "Por que a tentativa nº 3 é deplorável"). Outra ressalva é que, se A é uma classe de modelo, a sintaxe fica irritante (você sabe, você não pode usar mData1 diretamente, é necessário fazer isso-> mData1 e precisa começar a usar o tipo de texto e talvez as palavras-chave do modelo para tipos dependentes tipos de modelos, etc.). Outra ressalva é que você não pode mais usar o privado na classe original, apenas membros protegidos; portanto, não pode restringir o acesso de nenhuma classe herdada, não apenas o pimpl. Eu tentei, mas não pude contornar esse problema. Por exemplo, tentei tornar o pimpl uma classe de modelo de amigo na esperança de tornar a declaração de amigo suficientemente ampla para permitir que eu definisse a classe de pimpl real em um espaço de nome anônimo, mas isso simplesmente não funciona. Se alguém tiver alguma idéia de como manter os membros de dados privados e ainda permitir que uma classe pimpl herdada definida em um espaço de nome anônimo acesse esses, eu realmente gostaria de vê-lo! Isso eliminaria minha reserva principal de usar isso.
Porém, sinto que essas advertências são aceitáveis pelos benefícios do que proponho.
Tentei procurar online alguma referência a esse idioma "pimpl somente de função", mas não consegui encontrar nada. Estou realmente interessado no que as pessoas pensam sobre isso. Existem outros problemas com este ou os motivos pelos quais não devo usá-lo?
ATUALIZAR:
Eu encontrei essa proposta que mais ou menos tenta realizar exatamente o que sou, mas fazendo isso alterando o padrão. Eu concordo completamente com essa proposta e espero que ela se enquadre no padrão (não conheço nada desse processo, portanto não tenho idéia da probabilidade de isso acontecer). Eu preferiria que fosse possível fazer isso por meio de um mecanismo de linguagem incorporado. A proposta também explica os benefícios do que estou tentando alcançar muito melhor do que eu. Ele também não tem o problema de quebrar o encapsulamento, como minha sugestão tem (privado -> protegido). Ainda assim, até que a proposta chegue ao padrão (se isso acontecer), acho que minha sugestão possibilita esses benefícios, com as ressalvas que listei.
UPDATE2:
Uma das respostas menciona o LTO como uma possível alternativa para obter alguns dos benefícios (acho que otimizações mais agressivas). Não sei exatamente o que acontece em várias passagens de otimização do compilador, mas tenho um pouco de experiência com o código resultante (uso o gcc). Simplesmente colocar os métodos privados na classe original forçará aqueles a ter um vínculo externo.
Eu posso estar errado aqui, mas a maneira como interpreto isso é que o otimizador em tempo de compilação não pode eliminar a função, mesmo que todas as suas instâncias de chamada estejam completamente embutidas nessa TU. Por alguma razão, até o LTO se recusa a se livrar da definição da função, mesmo que pareça que todas as instâncias de chamada em todo o binário vinculado estejam todas inline. Encontrei algumas referências afirmando que é porque o vinculador não sabe se, de alguma forma, você ainda chamará a função usando ponteiros de função (embora eu não entenda por que o vinculador não consegue descobrir que o endereço desse método nunca é usado )
Este não é o caso se você usar minha sugestão e colocar esses métodos privados em um pimpl dentro de um espaço para nome anônimo. Se eles forem incorporados, as funções NÃO aparecerão (com -O3, que inclui -finline-functions) no arquivo de objeto.
Pelo que entendi, o otimizador, ao decidir se alinha ou não uma função, leva em consideração seu impacto no tamanho do código. Portanto, usando minha sugestão, estou tornando um pouco "mais barato" para o otimizador incorporar esses métodos particulares.
PCALL
é um comportamento indefinido. Você não pode converter umA
para umPImpl
e usá-lo, a menos que o objeto subjacente seja realmente do tipoPImpl
. No entanto, a menos que eu esteja enganado, os usuários apenas criarão objetos do tipoA
.