Estou lendo o livro "Excepcional C ++", de Herb Sutter, e nesse livro aprendi sobre o idioma pImpl. Basicamente, a idéia é criar uma estrutura para os private
objetos de a class
e alocá-los dinamicamente para diminuir o tempo de compilação (e também ocultar as implementações privadas de uma maneira melhor).
Por exemplo:
class X
{
private:
C c;
D d;
} ;
pode ser alterado para:
class X
{
private:
struct XImpl;
XImpl* pImpl;
};
e, no CPP, a definição:
struct X::XImpl
{
C c;
D d;
};
Isso parece bastante interessante, mas nunca vi esse tipo de abordagem antes, nem nas empresas em que trabalhei, nem em projetos de código aberto que vi o código-fonte. Então, eu estou querendo saber se esta técnica é realmente usada na prática?
Devo usá-lo em qualquer lugar ou com cautela? E essa técnica é recomendada para uso em sistemas embarcados (onde o desempenho é muito importante)?
fonte
struct XImpl : public X
. Isso parece mais natural para mim. Há algum outro problema que eu perdi?const unique_ptr<XImpl>
e nãoXImpl*
.Respostas:
Claro que é usado. Eu o uso no meu projeto, em quase todas as aulas.
Razões para usar o idioma PIMPL:
Compatibilidade binária
Ao desenvolver uma biblioteca, você pode adicionar / modificar campos
XImpl
sem interromper a compatibilidade binária com seu cliente (o que significaria falhas!). Como o layout binário daX
classe não muda quando você adiciona novos campos àXimpl
classe, é seguro adicionar novas funcionalidades à biblioteca em atualizações de versões secundárias.Obviamente, você também pode adicionar novos métodos não virtuais públicos / privados a
X
/XImpl
sem interromper a compatibilidade binária, mas isso é semelhante à técnica de cabeçalho / implementação padrão.Ocultar dados
Se você estiver desenvolvendo uma biblioteca, especialmente uma proprietária, pode ser desejável não divulgar quais outras bibliotecas / técnicas de implementação foram usadas para implementar a interface pública da sua biblioteca. Por causa de problemas de propriedade intelectual ou porque você acredita que os usuários podem ficar tentados a assumir suposições perigosas sobre a implementação ou apenas quebrar o encapsulamento usando terríveis truques de elenco. O PIMPL resolve / mitiga isso.
Tempo de compilação
O tempo de compilação diminui, pois somente o arquivo de origem (implementação)
X
precisa ser reconstruído quando você adiciona / remove campos e / ou métodos àXImpl
classe (que mapeia a adição de campos / métodos particulares na técnica padrão). Na prática, é uma operação comum.Com a técnica de cabeçalho / implementação padrão (sem PIMPL), quando você adiciona um novo campo a
X
, todo cliente que alocaX
(na pilha ou na pilha) precisa ser recompilado, porque deve ajustar o tamanho da alocação. Bem, todo cliente que nunca aloca o X também precisa ser recompilado, mas é apenas um overhead (o código resultante no lado do cliente será o mesmo).Além do mais, com a separação de cabeçalho / implementação padrão
XClient1.cpp
precisa ser recompilada mesmo quando um método privadoX::foo()
foi adicionadoX
eX.h
alterado, mesmo queXClient1.cpp
não seja possível chamá-lo por motivos de encapsulamento! Como acima, é pura sobrecarga e está relacionada à forma como os sistemas de construção C ++ da vida real funcionam.Obviamente, a recompilação não é necessária quando você apenas modifica a implementação dos métodos (porque você não toca no cabeçalho), mas isso é semelhante à técnica padrão de cabeçalho / implementação.
Isso depende de quão poderoso é o seu alvo. No entanto, a única resposta para essa pergunta é: meça e avalie o que você ganha e perde. Além disso, leve em consideração que, se você não estiver publicando uma biblioteca para ser usada em sistemas embarcados por seus clientes, apenas a vantagem do tempo de compilação se aplica!
fonte
Parece que muitas bibliotecas por aí usam para se manter estável em sua API, pelo menos para algumas versões.
Mas, como em todas as coisas, você nunca deve usar nada em qualquer lugar sem cautela. Sempre pense antes de usá-lo. Avalie quais vantagens ele oferece e se elas valem o preço que você paga.
As vantagens que isso pode lhe dar são:
Essas podem ou não ser vantagens reais para você. Como para mim, não me importo com alguns minutos de tempo de recompilação. Os usuários finais geralmente também não o fazem, pois sempre o compilam de uma vez e desde o início.
As possíveis desvantagens são (também aqui, dependendo da implementação e se são reais):
Portanto, dê um valor a tudo com cuidado e avalie-o por si mesmo. Para mim, quase sempre acontece que usar o idioma pimpl não vale a pena. Há apenas um caso em que eu o uso pessoalmente (ou pelo menos algo semelhante):
Meu wrapper C ++ para a
stat
chamada linux . Aqui a estrutura do cabeçalho C pode ser diferente, dependendo do que#defines
estiver definido. E como o cabeçalho do meu wrapper não pode controlar todos eles, apenas#include <sys/stat.h>
no meu.cxx
arquivo e evito esses problemas.fonte
File
classe (que expõe grande parte das informaçõesstat
retornaria no Unix) usa a mesma interface no Windows e no Unix, por exemplo.#ifdef
segundos para deixar o invólucro o mais fino possível. Mas todo mundo tem objetivos diferentes, o importante é reservar um tempo para pensar sobre isso, em vez de seguir cegamente alguma coisa.Concordo com todos os outros sobre os produtos, mas deixe-me colocar em evidência um limite: não funciona bem com modelos .
O motivo é que a instanciação do modelo requer a declaração completa disponível onde a instanciação ocorreu. (E esse é o principal motivo pelo qual você não vê os métodos de modelo definidos nos arquivos CPP)
Você ainda pode se referir às subclasses com modelo, mas como é necessário incluí-las todas, todas as vantagens do "desacoplamento da implementação" na compilação (evitando incluir todo o código específico de platoforma em todos os lugares, diminuindo a compilação) são perdidas.
É um bom paradigma para OOP clássico (baseado em herança), mas não para programação genérica (baseado em especialização).
fonte
Outras pessoas já forneceram as vantagens e desvantagens técnicas, mas acho que vale a pena notar o seguinte:
Em primeiro lugar, não seja dogmático. Se o pImpl funcionar para a sua situação, use-o - não o use apenas porque "é melhor OO, pois realmente oculta a implementação" etc. Citando a FAQ do C ++:
Apenas para dar um exemplo de software de código aberto onde ele é usado e por quê: OpenThreads, a biblioteca de encadeamentos usada pelo OpenSceneGraph . A idéia principal é remover do cabeçalho (por exemplo
<Thread.h>
) todo o código específico da plataforma, porque as variáveis de estado internas (por exemplo, identificadores de thread) diferem de plataforma para plataforma. Dessa forma, é possível compilar código na sua biblioteca sem o conhecimento das idiossincrasias das outras plataformas, porque tudo está oculto.fonte
Eu consideraria principalmente o PIMPL para classes expostas para serem usadas como API por outros módulos. Isso tem muitos benefícios, pois a recompilação das alterações feitas na implementação do PIMPL não afeta o restante do projeto. Além disso, para as classes de API, eles promovem uma compatibilidade binária (as alterações na implementação de um módulo não afetam os clientes desses módulos, não precisam ser recompiladas, pois a nova implementação tem a mesma interface binária - a interface exposta pelo PIMPL).
Quanto ao uso do PIMPL para todas as classes, eu consideraria cautela, pois todos esses benefícios têm um custo: é necessário um nível extra de indireção para acessar os métodos de implementação.
fonte
Eu acho que essa é uma das ferramentas mais fundamentais para dissociar.
Eu estava usando o pimpl (e muitos outros idiomas do Exceptional C ++) no projeto incorporado (SetTopBox).
O objetivo específico desse idoim em nosso projeto era ocultar os tipos que a classe XImpl usa. Especificamente, nós o usamos para ocultar detalhes de implementações para diferentes hardwares, nos quais cabeçalhos diferentes seriam acessados. Tivemos implementações diferentes das classes XImpl para uma plataforma e diferentes para a outra. O layout da classe X permaneceu o mesmo, independentemente da plataforma.
fonte
Eu costumava usar muito essa técnica no passado, mas depois me afastei dela.
Obviamente, é uma boa ideia ocultar os detalhes da implementação dos usuários da sua classe. No entanto, você também pode fazer isso fazendo com que os usuários da classe usem uma interface abstrata e que os detalhes da implementação sejam a classe concreta.
As vantagens do pImpl são:
Supondo que exista apenas uma implementação dessa interface, é mais claro não usar classe abstrata / implementação concreta
Se você tiver um conjunto de classes (um módulo) de modo que várias classes acessem o mesmo "impl", mas os usuários do módulo usarão apenas as classes "expostas".
Nenhuma tabela v se isso for considerado uma coisa ruim.
As desvantagens que encontrei do pImpl (onde a interface abstrata funciona melhor)
Embora você possa ter apenas uma implementação de "produção", usando uma interface abstrata, você também pode criar uma implementação "simulada" que funciona no teste de unidade.
(O maior problema). Antes dos dias de unique_ptr e mudança, você tinha opções restritas sobre como armazenar o pImpl. Um ponteiro bruto e você teve problemas com a sua classe como não copiável. Um auto_ptr antigo não funcionaria com a classe declarada posteriormente (nem todos os compiladores). Então, as pessoas começaram a usar shared_ptr, o que foi bom em tornar sua classe copiável, mas é claro que ambas as cópias tinham o mesmo shared_ptr subjacente que você não pode esperar (modifique uma e ambas sejam modificadas). Portanto, a solução geralmente era usar o ponteiro bruto para o interno e tornar a classe não copiável e retornar um shared_ptr para isso. Então, duas chamadas para novas. (Na verdade, 3 dado o shared_ptr antigo deu a você um segundo).
Tecnicamente, não é uma constante correta, pois a constância não é propagada para um ponteiro de membro.
Em geral, eu me afastei nos anos do pImpl e, em vez disso, usei a interface abstrata (e os métodos de fábrica para criar instâncias).
fonte
Como muitos outros disseram, o idioma Pimpl permite alcançar independência completa de ocultação e compilação de informações, infelizmente com o custo de perda de desempenho (indireto adicional do ponteiro) e necessidade adicional de memória (o próprio ponteiro do membro). O custo adicional pode ser crítico no desenvolvimento de software embarcado, principalmente nos cenários em que a memória deve ser economizada o máximo possível. O uso de classes abstratas C ++ como interfaces levaria aos mesmos benefícios pelo mesmo custo. Isso mostra, na verdade, uma grande deficiência de C ++, onde, sem a recorrência de interfaces do tipo C (métodos globais com um ponteiro opaco como parâmetro), não é possível ter uma verdadeira ocultação de informações e independência de compilação sem inconvenientes adicionais de recursos: isso ocorre principalmente porque o declaração de uma classe, que deve ser incluída por seus usuários,
fonte
Aqui está um cenário real que encontrei, em que esse idioma ajudou muito. Decidi recentemente oferecer suporte ao DirectX 11, bem como ao meu suporte existente ao DirectX 9, em um mecanismo de jogo. O mecanismo já incluiu a maioria dos recursos do DX; portanto, nenhuma das interfaces do DX foi usada diretamente; eles foram definidos apenas nos cabeçalhos como membros privados. O mecanismo utiliza DLLs como extensões, adicionando suporte a teclado, mouse, joystick e scripts, além de semanas como muitas outras extensões. Embora a maioria dessas DLLs não usasse o DX diretamente, elas exigiam conhecimento e vínculo com o DX simplesmente porque colocavam cabeçalhos que expunham o DX. Ao adicionar o DX 11, essa complexidade aumentaria dramaticamente, ainda que desnecessariamente. Mover os membros do DX para um Pimpl definido apenas na fonte eliminou essa imposição. Além dessa redução de dependências da biblioteca,
fonte
É usado na prática em muitos projetos. Sua utilidade depende muito do tipo de projeto. Um dos projetos mais importantes usando isso é o Qt , onde a idéia básica é ocultar o código de implementação ou plataforma específica do usuário (outros desenvolvedores usando o Qt).
Essa é uma ideia nobre, mas há uma verdadeira desvantagem: depuração Enquanto o código oculto nas implementações privadas é de qualidade premium, tudo está bem, mas, se houver erros, o usuário / desenvolvedor terá um problema, porque é apenas um ponteiro idiota para uma implementação oculta, mesmo que ele tenha o código-fonte das implementações.
Assim, como em quase todas as decisões de design, há prós e contras.
fonte
Um benefício que posso ver é que ele permite que o programador implemente determinadas operações de maneira bastante rápida:
PS: Espero não estar entendendo mal a semântica dos movimentos.
fonte