Idioma Pimpl vs interface de classe virtual Pure

118

Eu estava me perguntando o que faria um programador escolher o idioma Pimpl ou classe virtual pura e herança.

Eu entendo que o idioma pimpl vem com uma indireção extra explícita para cada método público e a sobrecarga de criação de objeto.

A classe virtual Pure, por outro lado, vem com indireção implícita (vtable) para a implementação de herança e eu entendo que nenhuma sobrecarga de criação de objeto.
EDIT : Mas você precisaria de uma fábrica se criar o objeto de fora

O que torna a classe virtual pura menos desejável do que o idioma pimpl?

Arkaitz Jimenez
fonte
3
Ótima pergunta, só queria perguntar a mesma coisa. Consulte também boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Respostas:

60

Ao escrever uma classe C ++, é apropriado pensar se ela será

  1. Um tipo de valor

    Cópia por valor, a identidade nunca é importante. É apropriado que seja uma chave em um std :: map. Por exemplo, uma classe de "string" ou uma classe de "data" ou uma classe de "número complexo". Faz sentido "copiar" instâncias de tal classe.

  2. Um tipo de entidade

    A identidade é importante. Sempre passado por referência, nunca por "valor". Freqüentemente, não faz sentido "copiar" as instâncias da classe. Quando faz sentido, um método polimórfico de "clone" geralmente é mais apropriado. Exemplos: uma classe Socket, uma classe Database, uma classe "política", qualquer coisa que seria um "fechamento" em uma linguagem funcional.

Tanto pImpl quanto a classe base abstrata pura são técnicas para reduzir as dependências do tempo de compilação.

No entanto, eu sempre uso pImpl para implementar tipos de valor (tipo 1) e apenas às vezes quando realmente quero minimizar o acoplamento e as dependências de tempo de compilação. Freqüentemente, não vale a pena se preocupar. Como você aponta corretamente, há mais sobrecarga sintática porque você precisa escrever métodos de encaminhamento para todos os métodos públicos. Para classes do tipo 2, sempre uso uma classe base abstrata pura com método (s) de fábrica associado (s).

Paul Hollingsworth
fonte
6
Por favor, veja o comentário de Paul de Vrieze a esta resposta . Pimpl e Pure Virtual diferem significativamente se você estiver em uma biblioteca e quiser trocar seu .so / .dll sem reconstruir o cliente. Os clientes se conectam aos front-ends do pimpl pelo nome, portanto, manter as assinaturas dos métodos antigos é o suficiente. OTOH, em caso abstrato puro, eles efetivamente vinculam por índice vtable, portanto, reordenar métodos ou inserir no meio quebrará a compatibilidade.
SnakE
1
Você só pode adicionar (ou reordenar) métodos em um front-end de classe Pimpl para manter a comparabilidade binária. Falando logicamente, você ainda mudou a interface e ela parece um pouco duvidosa. A resposta aqui é um equilíbrio sensato que também pode ajudar com o teste de unidade via "injeção de dependência"; mas a resposta sempre depende dos requisitos. Escritores de bibliotecas de terceiros (ao contrário de usar uma biblioteca em sua própria organização) podem preferir fortemente o Pimpl.
Spacen Jasset
31

Pointer to implementationé geralmente sobre como ocultar detalhes de implementação estrutural. Interfacessão sobre instanciar implementações diferentes. Eles realmente servem a dois propósitos diferentes.

D.Shawley
fonte
13
não necessariamente, já vi classes que armazenam vários pimpls dependendo da implementação desejada. Freqüentemente, é um win32 impl contra um linux impl de algo que precisa ser implementado de forma diferente por plataforma.
Doug T.
14
Mas você pode usar uma interface para separar os detalhes de implementação e ocultá-los
Arkaitz Jimenez
6
Embora você possa implementar o pimpl usando uma interface, geralmente não há razão para separar os detalhes de implementação. Portanto, não há razão para ser polimórfico. O motivo do pimpl é manter os detalhes de implementação longe do cliente (em C ++ para mantê-los fora do cabeçalho). Você pode fazer isso usando uma base / interface abstrata, mas geralmente isso é um exagero desnecessário.
Michael Burr
10
Por que é um exagero? Quero dizer, é mais lento o método de interface do que o pimpl? Pode haver razões lógicas, mas do ponto de vista prático, diria que é mais fácil fazer isso com uma interface abstrata
Arkaitz Jimenez
1
Eu diria que a interface / classe base abstrata é a maneira "normal" de fazer as coisas e permite testes mais fáceis por meio de simulação
paulm
28

O idioma pimpl ajuda a reduzir as dependências e os tempos de construção, especialmente em aplicativos grandes, e minimiza a exposição do cabeçalho dos detalhes de implementação de sua classe em uma unidade de compilação. Os usuários de sua classe nem precisam estar cientes da existência de uma espinha (exceto como um indicador enigmático do qual eles não têm conhecimento!).

Classes abstratas (virtuais puros) é algo que seus clientes devem estar cientes: se você tentar usá-los para reduzir o acoplamento e referências circulares, você precisa adicionar alguma forma de permitir que eles criem seus objetos (por exemplo, através de métodos de fábrica ou classes, injeção de dependência ou outros mecanismos).

Pontus Gagge
fonte
17

Eu estava procurando uma resposta para a mesma pergunta. Depois de ler alguns artigos e praticar , prefiro usar "interfaces de classes virtuais puras" .

  1. Eles são mais diretos (esta é uma opinião subjetiva). O idioma Pimpl me faz sentir que estou escrevendo código "para o compilador", não para o "próximo desenvolvedor" que lerá meu código.
  2. Alguns frameworks de teste têm suporte direto para simulação de classes virtuais puras
  3. É verdade que você precisa de uma fábrica acessível do lado de fora. Mas se você deseja alavancar o polimorfismo: isso também é "pró", não "contra". ... e um método de fábrica simples não dói tanto

A única desvantagem ( estou tentando investigar sobre isso ) é que o idioma pimpl pode ser mais rápido

  1. quando as chamadas de proxy são sequenciais, enquanto a herança necessariamente precisa de um acesso extra ao objeto VTABLE em tempo de execução
  2. a pegada de memória do pimpl public-proxy-class é menor (você pode fazer otimizações facilmente para trocas mais rápidas e outras otimizações semelhantes)
Ilias Bartolini
fonte
21
Lembre-se também de que, ao usar herança, você introduz uma dependência no layout vtable. Para manter a ABI, você não pode mais alterar as funções virtuais (adicionar no final é meio seguro, se não houver classes filhas que adicionem métodos virtuais próprios).
Paul de Vrieze
1
^ Este comentário aqui deve ser pegajoso.
CodeAngry de
10

Eu odeio espinhas! Eles fazem as aulas feias e ilegíveis. Todos os métodos são redirecionados para pimple. Você nunca vê nos cabeçalhos quais funcionalidades a classe possui, então você não pode refatorá-la (por exemplo, simplesmente alterar a visibilidade de um método). A aula parece "grávida". Acho que usar iterfaces é melhor e realmente o suficiente para ocultar a implementação do cliente. Você pode eventualmente permitir que uma classe implemente várias interfaces para mantê-los finos. Deve-se preferir interfaces! Nota: Você não precisa da classe de fábrica. O relevante é que os clientes da classe se comuniquem com suas instâncias por meio da interface apropriada. Considero a ocultação de métodos privados uma estranha paranóia e não vejo razão para isso, uma vez que temos interfaces.

Masha Ananieva
fonte
1
Existem alguns casos em que você não pode usar interfaces virtuais puras. Por exemplo, quando você tem algum código legado e dois módulos que você precisa separar sem tocá-los.
AlexTheo
Como @Paul de Vrieze apontou abaixo, você perde compatibilidade com a ABI ao alterar os métodos da classe base, porque você tem uma dependência implícita na vtable da classe. Depende do caso de uso, se isso é um problema.
H. Rittich
"A ocultação de métodos privados é uma estranha paranóia" Isso não permite ocultar as dependências e, portanto, minimizar o tempo de compilação se uma dependência mudar?
pooya13
Eu também não entendo como as fábricas são mais fáceis de refatorar do que pImpl. Você não sai da "interface" em ambos os casos e muda a implementação? Em Factory você tem que modificar um arquivo .h e um .cpp e em pImpl você tem que modificar um .h e dois arquivos .cpp mas isso é tudo e você geralmente não precisa modificar o arquivo cpp da interface do pImpl.
pooya 13
8

Há um problema muito real com as bibliotecas compartilhadas que o idioma pimpl contorna perfeitamente que os virtuais puros não: você não pode modificar / remover membros de dados de uma classe com segurança sem forçar os usuários da classe a recompilar seu código. Isso pode ser aceitável em algumas circunstâncias, mas não para bibliotecas de sistema, por exemplo.

Para explicar o problema em detalhes, considere o seguinte código em sua biblioteca / cabeçalho compartilhado:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

O compilador emite código na biblioteca compartilhada que calcula o endereço do inteiro a ser inicializado com um certo deslocamento (provavelmente zero neste caso, porque é o único membro) do ponteiro para o objeto A que ele sabe ser this .

No lado do usuário do código, um new Aprimeiro alocará sizeof(A)bytes de memória e, em seguida, passará um ponteiro para essa memória ao A::A()construtor comothis .

Se em uma revisão posterior de sua biblioteca você decidir descartar o inteiro, torná-lo maior, menor ou adicionar membros, haverá uma incompatibilidade entre a quantidade de memória alocada pelo código do usuário e os deslocamentos que o código do construtor espera. O resultado provável é um travamento, se você tiver sorte - se você tiver menos sorte, seu software se comporta de maneira estranha.

Ao pimpl'ing, você pode adicionar e remover membros de dados da classe interna com segurança, conforme a alocação de memória e a chamada do construtor acontecem na biblioteca compartilhada:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Tudo o que você precisa fazer agora é manter sua interface pública livre de membros de dados que não sejam o ponteiro para o objeto de implementação, e você estará protegido contra essa classe de erros.

Edit: talvez eu deva acrescentar que a única razão pela qual estou falando sobre o construtor aqui é que eu não queria fornecer mais código - a mesma argumentação se aplica a todas as funções que acessam membros de dados.


fonte
4
Em vez de void *, acho que é mais tradicional declarar a classe de implementação:class A_impl *impl_;
Frank Krueger
9
Não entendo, você não deve declarar membros privados em uma classe virtual pura que pretende usar como interface, a ideia é manter a classe basicamente abstrata, sem tamanho, apenas métodos virtuais puros, não vejo nada você não pode fazer por meio de bibliotecas compartilhadas
Arkaitz Jimenez
@Frank Krueger: Você está certo, eu estava sendo preguiçoso. @Arkaitz Jimenez: Pequeno mal-entendido; se você tem uma classe que contém apenas funções virtuais puras, então não há muito sentido em falar sobre bibliotecas compartilhadas. Por outro lado, se você estiver lidando com bibliotecas compartilhadas, pimpl'ing suas classes públicas pode ser prudente pelo motivo descrito acima.
10
Isso é simplesmente incorreto. Ambos os métodos permitem ocultar o estado de implementação de suas classes, se você fizer de sua outra classe uma classe de "base abstrata pura".
Paul Hollingsworth
10
A primeira frase em seu anser implica que virtuais puros com um método de fábrica associado de alguma forma não permitem ocultar o estado interno da classe. Isso não é verdade. Ambas as técnicas permitem ocultar o estado interno da classe. A diferença é a aparência para o usuário. pImpl permite que você ainda represente uma classe com semântica de valor, mas também oculte o estado interno. Pure Abstract Base Class + método de fábrica permite representar tipos de entidade, e também permite ocultar o estado interno. O último é exatamente como o COM funciona. O capítulo 1 de "Essential COM" tem uma grande discussão sobre isso.
Paul Hollingsworth
6

Não devemos esquecer que a herança é um acoplamento mais forte e mais próximo do que a delegação. Eu também levaria em consideração todas as questões levantadas nas respostas dadas ao decidir quais expressões de design usar para resolver um problema específico.

Sam
fonte
3

Embora amplamente coberto nas outras respostas, talvez eu possa ser um pouco mais explícito sobre um dos benefícios do pimpl sobre as classes base virtuais:

Uma abordagem pimpl é transparente do ponto de vista do usuário, o que significa que você pode, por exemplo, criar objetos da classe na pilha e usá-los diretamente em contêineres. Se você tentar ocultar a implementação usando uma classe base virtual abstrata, você precisará retornar um ponteiro compartilhado para a classe base de uma fábrica, complicando seu uso. Considere o seguinte código de cliente equivalente:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();
Superfly Jon
fonte
2

No meu entendimento, essas duas coisas servem a propósitos completamente diferentes. O objetivo do idioma pimple é basicamente fornecer um controle para sua implementação para que você possa fazer coisas como trocas rápidas por uma classificação.

O propósito das classes virtuais é mais na linha de permitir o polimorfismo, ou seja, você tem um ponteiro desconhecido para um objeto de um tipo derivado e quando você chama a função x você sempre obtém a função certa para qualquer classe para a qual o ponteiro base realmente aponta.

Maçãs e laranjas, na verdade.

Robert S. Barnes
fonte
Eu concordo com as maçãs / laranjas. Mas parece que você usa pImpl para um funcional. Meu objetivo é principalmente ocultar informações técnicas e de construção.
xtofl
2

O problema mais irritante sobre o idioma pimpl é que torna extremamente difícil manter e analisar o código existente. Portanto, usando o pimpl, você paga com o tempo e a frustração do desenvolvedor apenas para "reduzir as dependências e os tempos de construção e minimizar a exposição do cabeçalho dos detalhes de implementação". Decida você mesmo, se realmente vale a pena.

Especialmente os "tempos de construção" é um problema que você pode resolver com um hardware melhor ou usando ferramentas como o Incredibuild (www.incredibuild.com, também já incluído no Visual Studio 2017), não afetando o design do seu software. O design do software deve ser geralmente independente da forma como o software é construído.

Trantor
fonte
Você também paga com o tempo de desenvolvedor quando o tempo de construção é de 20 minutos em vez de 2, então é um pouco de equilíbrio, um sistema de módulo real ajudaria muito aqui.
Arkaitz Jimenez
IMHO, a forma como o software é construído não deve influenciar o design interno de forma alguma. Este é um problema completamente diferente.
Trantor
2
O que torna isso difícil de analisar? Um monte de chamadas em um arquivo de implementação encaminhado para a classe Impl não parece difícil.
mabraham
2
Imagine implementações de depuração onde pimpl e interfaces são usados. Começando com uma chamada no código do usuário A, você rastreia a interface B, pula para a classe C espinhada para finalmente começar a depurar a classe de implementação D ... Quatro etapas até que você possa analisar o que realmente acontece. E se tudo for implementado em uma DLL, você possivelmente encontrará uma interface C em algum lugar entre ....
Trantor
Por que você usaria uma interface com pImpl quando pImpl também pode fazer o trabalho de uma interface? (ou seja, pode ajudá-lo a alcançar a inversão de dependência)
pooya13