O idioma pImpl é realmente usado na prática?

165

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 privateobjetos de a classe 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)?

Renan Greinert
fonte
É essencialmente o mesmo que decidir que X é uma interface (abstrata) e Ximpl é a implementação? struct XImpl : public X. Isso parece mais natural para mim. Há algum outro problema que eu perdi?
Aaron McDaid
@AaronMcDaid: É semelhante, mas tem as vantagens de que (a) as funções de membro não precisam ser virtuais e (b) você não precisa de uma fábrica ou a definição da classe de implementação para instancia-la.
Mike Seymour
2
@AaronMcDaid O idioma pimpl evita chamadas de função virtual. Também é um pouco mais C ++ - ish (para alguma concepção do C ++ - ish); você invoca construtores, em vez de funções de fábrica. Eu usei os dois, dependendo do que está na base de código existente --- o idioma pimpl (originalmente chamado de idioma do gato de Cheshire e anterior à descrição de Herb por pelo menos 5 anos) parece ter uma história mais longa e ser mais amplamente utilizado em C ++, mas, caso contrário, ambos funcionam.
precisa saber é o seguinte
30
Em C ++, o pimpl deve ser implementado com const unique_ptr<XImpl>e não XImpl*.
Neil G
1
"nunca vi esse tipo de abordagem antes, nem nas empresas em que trabalhei, nem em projetos de código aberto". Qt quase nunca está usando NÃO.
precisa saber é o seguinte

Respostas:

132

Então, eu estou querendo saber se esta técnica é realmente usada na prática? Devo usá-lo em qualquer lugar ou com cautela?

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 XImplsem interromper a compatibilidade binária com seu cliente (o que significaria falhas!). Como o layout binário da Xclasse não muda quando você adiciona novos campos à Ximplclasse, é 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/ XImplsem 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) Xprecisa ser reconstruído quando você adiciona / remove campos e / ou métodos à XImplclasse (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 aloca X(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.cppprecisa ser recompilada mesmo quando um método privado X::foo()foi adicionado Xe X.halterado, mesmo que XClient1.cppnã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.


Essa técnica é recomendada para uso em sistemas embarcados (onde o desempenho é muito importante)?

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!

BЈовић
fonte
16
+1 porque é amplamente utilizado na empresa pela qual trabalho também e pelos mesmos motivos.
Benoit
9
também, compatibilidade binária
Ambroz Bizjak
9
Na biblioteca Qt, esse método também é usado em situações de ponteiro inteligente. Portanto, o QString mantém seu conteúdo como uma classe imutável internamente. Quando a classe pública é "copiada", o ponteiro do membro privado é copiado em vez de toda a classe privada. Estas aulas particulares depois também usar ponteiros inteligentes, então você basicamente obter a coleta de lixo com a maioria das classes, além de muito melhor desempenho devido ao ponteiro copiar em vez de classe cheia de copiar
Timothy Baldridge
8
Ainda mais, com o idioma pimpl, o Qt pode manter a compatibilidade binária para frente e para trás em uma única versão principal (na maioria dos casos). Na IMO, essa é de longe a razão mais significativa para usá-lo.
whitequark
1
Também é útil para implementar código específico da plataforma, pois você pode manter a mesma API.
doc
49

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:

  • ajuda a manter a compatibilidade binária de bibliotecas compartilhadas
  • ocultando certos detalhes internos
  • ciclos decrescentes de recompilaçã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):

  • Aumento no uso de memória devido a mais alocações do que com a variante ingênua
  • maior esforço de manutenção (você precisa escrever pelo menos as funções de encaminhamento)
  • perda de desempenho (o compilador pode não ser capaz de incorporar coisas incorporadas, como ocorre com uma implementação ingênua da sua classe)

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 statchamada linux . Aqui a estrutura do cabeçalho C pode ser diferente, dependendo do que #definesestiver definido. E como o cabeçalho do meu wrapper não pode controlar todos eles, apenas #include <sys/stat.h>no meu .cxxarquivo e evito esses problemas.

PlasmaHH
fonte
2
Quase sempre deve ser usado para interfaces de sistema, para tornar o sistema de código de interface independente. Minha Fileclasse (que expõe grande parte das informações statretornaria no Unix) usa a mesma interface no Windows e no Unix, por exemplo.
James Kanze
5
@ JamesKanze: Mesmo lá, eu pessoalmente sentava por um momento e pensava se talvez não fosse suficiente ter alguns #ifdefsegundos 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.
PlasmaHH
31

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).

Emilio Garavaglia
fonte
4
Você precisa ser mais preciso: não há absolutamente nenhum problema ao usar as classes PIMPL como argumentos de tipo de modelo. Somente se a própria classe de implementação precisar ser parametrizada nos argumentos do modelo da classe externa, ela não poderá mais ser ocultada do cabeçalho da interface, mesmo que ainda seja uma classe privada. Se você pode excluir o argumento do modelo, certamente ainda poderá executar o PIMPL "adequado". Com a exclusão de tipo, você também pode executar o PIMPL em uma classe não modelo de base e fazer com que a classe de modelo derive dela.
Reintegrar Monica
22

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 ++:

encapsulamento é para código, não para pessoas ( fonte )

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.

azálea
fonte
12

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.

Ghita
fonte
"um nível extra de indireção é necessário para acessar os métodos de implementação." Isto é?
xaxxon
@xaxxon sim, é. pimpl é mais lento se os métodos forem de baixo nível. nunca use-o para coisas que vivem em um circuito fechado, por exemplo.
Erik Aronesty 04/12/19
@xaxxon Eu diria que, em geral, é necessário um nível extra. Se o embutimento for realizado, não. Mas inlinning não seria uma opção no código compilado em uma dll diferente.
Ghita
5

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.

user377178
fonte
4

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:

  1. Supondo que exista apenas uma implementação dessa interface, é mais claro não usar classe abstrata / implementação concreta

  2. 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".

  3. Nenhuma tabela v se isso for considerado uma coisa ruim.

As desvantagens que encontrei do pImpl (onde a interface abstrata funciona melhor)

  1. 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.

  2. (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).

  3. 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).

CashCow
fonte
3

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,

ncsc
fonte
3

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,

Kit10
fonte
2

É 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.

Holger Kretzschmar
fonte
9
é idiota, mas é digitado ... por que você não pode seguir o código no depurador?
UncloudErro
2
De um modo geral, para depurar no código Qt, você precisa criar o Qt você mesmo. Depois de fazer isso, não há problema em entrar nos métodos do PIMPL e inspecionar o conteúdo dos dados do PIMPL.
Reintegrar Monica
0

Um benefício que posso ver é que ele permite que o programador implemente determinadas operações de maneira bastante rápida:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: Espero não estar entendendo mal a semântica dos movimentos.

BenGoldberg
fonte