Essa é uma boa abordagem para uma hierarquia de classes baseada em "pImpl" em C ++?

9

Eu tenho uma hierarquia de classes para a qual gostaria de separar a interface da implementação. Minha solução é ter duas hierarquias: uma hierarquia de classes de identificador para a interface e uma hierarquia de classes não pública para a implementação. A classe de identificador base possui um ponteiro para implementação que as classes de identificador derivadas convertem em um ponteiro do tipo derivado (consulte a função getPimpl()).

Aqui está um esboço da minha solução para uma classe base com duas classes derivadas. Existe uma solução melhor?

Arquivo "Base.h":

#include <memory>

class Base {
protected:
    class Impl;
    std::shared_ptr<Impl> pImpl;
    Base(Impl* pImpl) : pImpl{pImpl} {};
    ...
};

class Derived_1 final : public Base {
protected:
    class Impl;
    inline Derived_1* getPimpl() const noexcept {
        return reinterpret_cast<Impl*>(pImpl.get());
    }
public:
    Derived_1(...);
    void func_1(...) const;
    ...
};

class Derived_2 final : public Base {
protected:
    class Impl;
    inline Derived_2* getPimpl() const noexcept {
        return reinterpret_cast<Impl*>(pImpl.get());
    }
public:
    Derived_2(...);
    void func_2(...) const;
    ...
};

Arquivo "Base.cpp":

class Base::Impl {
public:
    Impl(...) {...}
    ...
};

class Derived_1::Impl final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_1(...) {...}
    ...
};

class Derived_2::Impl final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_2(...) {...}
    ...
};

Derived_1::Derived_1(...) : Base(new Derived_1::Impl(...)) {...}
Derived_1::func_1(...) const { getPimpl()->func_1(...); }

Derived_2::Derived_2(...) : Base(new Derived_2::Impl(...)) {...}
Derived_2::func_2(...) const { getPimpl()->func_2(...); }
Steve Emmerson
fonte
Qual dessas classes será visível do lado de fora da biblioteca / componente? Se apenas Base, uma classe base abstrata normal ("interface") e implementações concretas sem pimpl podem ser suficientes.
D. Jurcau 12/01
@ D.Jurcau As classes base e derivada serão todas visíveis publicamente. Obviamente, as classes de implementação não.
9788 Steve Jobs Emerson,
Por que downcast? A classe base está em uma posição estranha aqui, pode ser substituída por um ponteiro compartilhado, com melhor segurança na digitação e menos código.
Basilevs
@Basilevs eu não entendo. A classe base pública usa o idioma pimpl para ocultar a implementação. Não vejo como substituí-lo por um ponteiro compartilhado pode manter a hierarquia de classes sem converter ou duplicar o ponteiro. Você pode fornecer um exemplo de código?
precisa saber é o seguinte
Proponho duplicar o ponteiro, em vez de replicar o downcast.
Basilevs

Respostas:

1

Eu acho que é uma péssima estratégia para Derived_1::Implderivar Base::Impl.

O principal objetivo do uso do idioma Pimpl é ocultar os detalhes de implementação de uma classe. Ao deixar Derived_1::Implderivar Base::Impl, você derrotou esse objetivo. Agora, não apenas a implementação de Basedepende Base::Impl, a implementação de Derived_1também depende Base::Impl.

Existe uma solução melhor?

Isso depende de quais trade-offs são aceitáveis ​​para você.

Solução 1

Torne as Implaulas totalmente independentes. Isso implica que haverá dois ponteiros para as Implclasses - um Basee outro Derived_N.

class Base {

   protected:
      Base() : pImpl{new Impl()} {}

   private:
      // It's own Impl class and pointer.
      class Impl { };
      std::shared_ptr<Impl> pImpl;

};

class Derived_1 final : public Base {
   public:
      Derived_1() : Base(), pImpl{new Impl()} {}
      void func_1() const;
   private:
      // It's own Impl class and pointer.
      class Impl { };
      std::shared_ptr<Impl> pImpl;
};

Solução 2

Exponha as classes apenas como alças. Não exponha as definições e implementações de classe.

Arquivo de cabeçalho público:

struct Handle {unsigned long id;};
struct Derived1_tag {};
struct Derived2_tag {};

Handle constructObject(Derived1_tag tag);
Handle constructObject(Derived2_tag tag);

void deleteObject(Handle h);

void fun(Handle h, Derived1_tag tag);
void bar(Handle h, Derived2_tag tag); 

Aqui está uma implementação rápida

#include <map>

class Base
{
   public:
      virtual ~Base() {}
};

class Derived1 : public Base
{
};

class Derived2 : public Base
{
};

namespace Base_Impl
{
   struct CompareHandle
   {
      bool operator()(Handle h1, Handle h2) const
      {
         return (h1.id < h2.id);
      }
   };

   using ObjectMap = std::map<Handle, Base*, CompareHandle>;

   ObjectMap& getObjectMap()
   {
      static ObjectMap theMap;
      return theMap;
   }

   unsigned long getNextID()
   {
      static unsigned id = 0;
      return ++id;
   }

   Handle getHandle(Base* obj)
   {
      auto id = getNextID();
      Handle h{id};
      getObjectMap()[h] = obj;
      return h;
   }

   Base* getObject(Handle h)
   {
      return getObjectMap()[h];
   }

   template <typename Der>
      Der* getObject(Handle h)
      {
         return dynamic_cast<Der*>(getObject(h));
      }
};

using namespace Base_Impl;

Handle constructObject(Derived1_tag tag)
{
   // Construct an object of type Derived1
   Derived1* obj = new Derived1;

   // Get a handle to the object and return it.
   return getHandle(obj);
}

Handle constructObject(Derived2_tag tag)
{
   // Construct an object of type Derived2
   Derived2* obj = new Derived2;

   // Get a handle to the object and return it.
   return getHandle(obj);
}

void deleteObject(Handle h)
{
   // Get a pointer to Base given the Handle.
   //
   Base* obj = getObject(h);

   // Remove it from the map.
   // Delete the object.
   if ( obj != nullptr )
   {
      getObjectMap().erase(h);
      delete obj;
   }
}

void fun(Handle h, Derived1_tag tag)
{
   // Get a pointer to Derived1 given the Handle.
   Derived1* obj = getObject<Derived1>(h);
   if ( obj == nullptr )
   {
      // Problem.
      // Decide how to deal with it.

      return;
   }

   // Use obj
}

void bar(Handle h, Derived2_tag tag)
{
   Derived2* obj = getObject<Derived2>(h);
   if ( obj == nullptr )
   {
      // Problem.
      // Decide how to deal with it.

      return;
   }

   // Use obj
}

Prós e contras

Com a primeira abordagem, você pode construir Derivedclasses na pilha. Com a segunda abordagem, isso não é uma opção.

Com a primeira abordagem, você incorre no custo de duas alocações e desalocações dinâmicas para construir e destruir um Derivedna pilha. Se você construir e destruir um Derivedobjeto da pilha, incorre no custo de mais uma alocação e desalocação. Com a segunda abordagem, você incorre apenas no custo de uma alocação dinâmica e uma desalocação para cada objeto.

Com a primeira abordagem, você tem a capacidade de usar a virtualfunção membro is Base. Com a segunda abordagem, isso não é uma opção.

Minha sugestão

Eu usaria a primeira solução para poder usar a hierarquia de classes e as virtualfunções de membro Base, embora isso seja um pouco mais caro.

R Sahu
fonte
0

A única melhoria que posso ver aqui é permitir que as classes concretas definam o campo de implementação. Se as classes base abstratas precisarem, elas podem definir uma propriedade abstrata que seja fácil de implementar nas classes concretas:

Base.h

class Base {
protected:
    class Impl;
    virtual std::shared_ptr<Impl> getImpl() =0;
    ...
};

class Derived_1 final : public Base {
protected:
    class Impl1;
    std::shared_ptr<Impl1> pImpl
    virtual std::shared_ptr<Base::Impl> getImpl();
public:
    Derived_1(...);
    void func_1(...) const;
    ...
};

Base.cpp

class Base::Impl {
public:
    Impl(...) {...}
    ...
};

class Derived_1::Impl1 final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_1(...) {...}
    ...
};

std::shared_ptr<Base::Impl> Derived_1::getImpl() { return pPimpl; }
Derived_1::Derived_1(...) : pPimpl(std::make_shared<Impl1>(...)) {...}
void Derived_1::func_1(...) const { pPimpl->func_1(...); }

Isso parece ser mais seguro para mim. Se você tem uma árvore grande, também pode introduzir virtual std::shared_ptr<Impl1> getImpl1() =0no meio da árvore.

peruca
fonte