Qual é o objetivo do padrão PImpl enquanto podemos usar a interface para o mesmo objetivo em C ++?


49

Eu vejo muito código-fonte que usa o idioma PImpl em C ++. Suponho que seu objetivo seja ocultar os dados / tipo / implementação privados, para que ele possa remover a dependência e reduzir o tempo de compilação e o problema de inclusão de cabeçalho.

Mas as classes interface / puro-abstrato em C ++ também têm esse recurso, elas também podem ser usadas para ocultar dados / tipo / implementação. E para permitir que o chamador veja apenas a interface ao criar um objeto, podemos declarar um método de fábrica no cabeçalho da interface.

A comparação é:

  1. Custo :

    O custo da maneira da interface é menor, porque você nem precisa repetir a implementação da função de wrapper público void Bar::doWork() { return m_impl->doWork(); }, basta definir a assinatura na interface.

  2. Bem entendido :

    A tecnologia de interface é melhor compreendida por todo desenvolvedor de C ++.

  3. Desempenho :

    O desempenho da interface não é pior que o idioma PImpl, um acesso extra à memória. Presumo que o desempenho seja o mesmo.

A seguir, está o pseudocódigo para ilustrar minha pergunta:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

O mesmo objetivo pode ser implementado usando a interface:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Então, qual é o objetivo do PImpl?

Respostas:


50

Primeiro, o PImpl é geralmente usado para classes não polimórficas. E quando uma classe polimórfica tem PImpl, ela geralmente permanece polimórfica, que ainda implementa interfaces e substitui métodos virtuais da classe base e assim por diante. Portanto, a implementação mais simples do PImpl não é interface, é uma classe simples que contém diretamente os membros!

Há três razões para usar o PImpl:

  1. Tornando a interface binária (ABI) independente dos membros privados. É possível atualizar uma biblioteca compartilhada sem recompilar o código dependente, mas apenas enquanto a interface binária permanecer a mesma. Agora, quase qualquer alteração no cabeçalho, exceto a adição de uma função não membro e a adição de uma função membro não virtual, altera a ABI. O idioma PImpl move a definição dos membros privados para a fonte e, assim, separa a ABI de sua definição. Consulte Problema na interface binária frágil

  2. Quando um cabeçalho é alterado, todas as fontes, incluindo ele, precisam ser recompiladas. E a compilação do C ++ é bastante lenta. Portanto, movendo as definições dos membros privados para a origem, o idioma PImpl reduz o tempo de compilação, pois menos dependências precisam ser puxadas no cabeçalho e reduz o tempo de compilação após modificações ainda mais, pois os dependentes não precisam ser recompilados (ok, isso também se aplica à interface + função de fábrica com classe de concreto oculta).

  3. Para muitas classes em C ++, a segurança de exceção é uma propriedade importante. Frequentemente, você precisa compor várias classes em uma, para que, se durante a operação em mais de um membro, nenhum deles seja modificado ou você tenha uma operação que deixará o membro em estado inconsistente se ele for lançado e você precisar que o objeto contenha permaneça consistente. Nesse caso, você implementa a operação criando uma nova instância do PImpl e as troca quando a operação é bem-sucedida.

Na verdade, a interface também pode ser usada apenas para ocultar a implementação, mas possui as seguintes desvantagens:

  1. Adicionar método não virtual não quebra a ABI, mas adicionar um método virtual quebra. Portanto, as interfaces não permitem a adição de métodos, o PImpl.

  2. O Inteface só pode ser usado via ponteiro / referência; portanto, o usuário precisa cuidar do gerenciamento adequado dos recursos. Por outro lado, as classes que usam o PImpl ainda são tipos de valor e manipulam os recursos internamente.

  3. A implementação oculta não pode ser herdada, a classe com o PImpl pode.

E, é claro, a interface não ajudará na segurança de exceções. Você precisa do indireção dentro da classe para isso.


Bom ponto. Adicionar função pública na Implclasse não quebrará o ABI, mas adicionar função pública na interface quebrará ABI.
ZijingWu 03/10

11
Adicionar uma função / método público não precisa quebrar a ABI; mudanças estritamente aditivas não estão quebrando mudanças. No entanto, você deve ter sempre muito cuidado; o código que medeia entre o front-end e o back-end precisa lidar com todo tipo de diversão com o versionamento. Tudo fica complicado. (Este tipo de coisa é por isso que um número de projetos de software preferem C para C ++, que lhes permite obter um controlo mais apertado sobre exatamente o que o ABI é.)
Donal Fellows

3
@DonalFellows: Adicionar uma função / método público não quebra a ABI. A adição de um método virtual faz e faz isso sempre , a menos que o usuário seja impedido de herdar o objeto (que o PImpl obtém).
Jan Hudec

4
Para esclarecer por que a adição de um método virtual interrompe a ABI. Tem a ver com ligação. A vinculação a métodos não virtuais é por nome, independentemente de a biblioteca ser estática ou dinâmica. Adicionar outro método não virtual é apenas adicionar outro nome ao qual vincular. No entanto, os métodos virtuais são índices em uma tabela, e o código externo efetivamente se vincula por índice. A adição de um método virtual pode mover outros métodos na vtable sem que o código externo saiba.
Cobra

11
O PImpl também reduz o tempo de compilação inicial. Por exemplo, se a minha implementação tem um campo de algum tipo de modelo (por exemplo unordered_set), sem Pimpl, eu preciso #include <unordered_set>no MyClass.h. Com Pimpl, só precisa incluí-lo no .cpparquivo, não o .harquivo e, portanto, tudo o resto que inclui isso ...
Martin C. Martin

7

Eu só quero abordar o seu ponto de desempenho. Se você usa uma interface, você necessariamente criou funções virtuais que NÃO serão incorporadas pelo otimizador do compilador. As funções PIMPL podem (e provavelmente serão, porque são muito curtas) ser incorporadas (talvez mais de uma vez se a função IMPL também for pequena). Uma chamada de função virtual não pode ser otimizada, a menos que você use otimizações completas de análise de programa que levam muito tempo para serem realizadas.

Se sua classe PIMPL não for usada de maneira crítica ao desempenho, esse ponto não importará muito, mas sua suposição de que o desempenho é o mesmo vale apenas em algumas situações, não em todas.


11
Com o uso da otimização do tempo do link, a maior parte da sobrecarga do uso do idioma pimpl é removida. Tanto as funções do wrapper externo quanto as funções de implementação podem ser incorporadas, fazendo as chamadas de função efetivamente como uma classe normal implementada dentro de um cabeçalho.
Goji7

Isso só é verdade se você usar a otimização do tempo do link. O ponto do PImpl é que o compilador não possui a implementação, nem mesmo a função de encaminhamento. O vinculador faz embora.
Martin C. Martin

5

Esta página responde à sua pergunta. Muitas pessoas concordam com sua afirmação. O uso do Pimpl tem as seguintes vantagens sobre os campos / membros privados.

  1. A alteração de variáveis ​​de membros particulares de uma classe não requer a recompilação de classes que dependem dela, tornando os tempos mais rápidos e o FragileBinaryInterfaceProblem é reduzido.
  2. O arquivo de cabeçalho não precisa # incluir classes usadas 'por valor' em variáveis ​​de membros particulares, portanto, os tempos de compilação são mais rápidos.

O idioma Pimpl é uma otimização em tempo de compilação, uma técnica de quebra de dependência. Reduz arquivos de cabeçalho grandes. Reduz o cabeçalho apenas para a interface pública. O comprimento do cabeçalho é importante no C ++ quando você pensa em como ele funciona. #include concatena efetivamente o arquivo de cabeçalho com o arquivo de origem - lembre-se de que as unidades C ++ pré-processadas podem ser muito grandes. O uso do Pimpl pode melhorar o tempo de compilação.

Existem outras técnicas melhores de quebra de dependência - que envolvem a melhoria do seu design. O que os métodos privados representam? Qual é a responsabilidade única? Eles deveriam ser outra classe?


O PImpl não é uma otimização em relação ao tempo de execução. As alocações não são triviais e pimpl significa alocação extra. É uma técnica de quebra de dependência e otimização apenas do tempo de compilação .
Jan Hudec

@JanHudec esclarecido.
Dave Hillier

@DaveHillier, +1 para menção FragileBinaryInterfaceProblem
ZijingWu
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.