¿Es este un buen enfoque para una jerarquía de clases basada en "pImpl" en C ++?


9

Tengo una jerarquía de clases para la que me gustaría separar la interfaz de la implementación. Mi solución es tener dos jerarquías: una jerarquía de clase de identificador para la interfaz y una jerarquía de clase no pública para la implementación. La clase de identificador base tiene un puntero a implementación que las clases de identificador derivadas convierten a un puntero del tipo derivado (ver función getPimpl()).

Aquí hay un boceto de mi solución para una clase base con dos clases derivadas. ¿Hay una mejor solución?

Archivo "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;
    ...
};

Archivo "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(...); }

¿Cuál de estas clases será visible desde el exterior de la biblioteca / componente? Si solo Base, una clase base abstracta normal ("interfaz") e implementaciones concretas sin pimpl podrían ser suficientes.
D. Jurcau

@ D.Jurcau Las clases base y derivadas serán visibles públicamente. Obviamente, las clases de implementación no lo harán.
Steve Emmerson

¿Por qué abatido? La clase base está en una posición extraña aquí, se puede reemplazar con un puntero compartido con seguridad de tipografía mejorada y menos código.
Basilevs

@Basilevs no entiendo. La clase de base pública utiliza el idioma pimpl para ocultar la implementación. No veo cómo reemplazarlo con un puntero compartido puede mantener la jerarquía de clases sin convertir o duplicar el puntero. ¿Puedes proporcionar un ejemplo de código?
Steve Emmerson el

Propongo duplicar el puntero, en lugar de replicar downcast.
Basilevs

Respuestas:


1

Creo que es una estrategia pobre de la que Derived_1::Implderivar Base::Impl.

El propósito principal de usar el idioma Pimpl es ocultar los detalles de implementación de una clase. Al dejar Derived_1::Implderivar de Base::Impl, has derrotado ese propósito. Ahora, no solo Basedepende Base::Implla implementación de , sino que Derived_1también depende de la implementación de Base::Impl.

¿Hay una mejor solución?

Eso depende de qué compensaciones sean aceptables para usted.

Solución 1

Haz Implclases totalmente independientes. Esto implicará que habrá dos punteros a las Implclases: uno en Basey otro en 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;
};

Solución 2

Exponga las clases solo como identificadores. No exponga las definiciones de clase e implementaciones en absoluto.

Archivo de encabezado 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); 

Aquí está la implementación 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
}

Pros y contras

Con el primer enfoque, puede construir Derivedclases en la pila. Con el segundo enfoque, esa no es una opción.

Con el primer enfoque, incurrirá en el costo de dos asignaciones dinámicas y desasignaciones para construir y destruir un Deriveden la pila. Si construye y destruye un Derivedobjeto del montón, incurra en el costo de una asignación y desasignación más. Con el segundo enfoque, solo incurrirá en el costo de una asignación dinámica y una desasignación por cada objeto.

Con el primer enfoque, puede usar la virtualfunción miembro es Base. Con el segundo enfoque, esa no es una opción.

Mi sugerencia

Iría con la primera solución para poder usar la jerarquía de clases y las virtualfunciones de miembro Basea pesar de que es un poco más caro.


0

La única mejora que puedo ver aquí es dejar que las clases concretas definan el campo de implementación. Si las clases base abstractas lo necesitan, pueden definir una propiedad abstracta que sea fácil de implementar en las clases 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(...); }

Esto parece ser más seguro para mí. Si tiene un árbol grande, también puede introducirlo virtual std::shared_ptr<Impl1> getImpl1() =0en el medio del árbol.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.