Apakah ini pendekatan yang baik untuk hirarki kelas berbasis "pImpl" di C ++?


9

Saya memiliki hierarki kelas yang ingin saya pisahkan antarmuka dari implementasinya. Solusi saya adalah memiliki dua hierarki: hierarki kelas pegangan untuk antarmuka dan hierarki kelas non-publik untuk implementasi. Kelas pegangan dasar memiliki pointer-to-implementasi yang kelas pegangan turunan dilemparkan ke pointer dari tipe turunan (lihat fungsi getPimpl()).

Inilah sketsa solusi saya untuk kelas dasar dengan dua kelas turunan. Apakah ada solusi yang lebih baik?

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

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

Manakah dari kelas-kelas ini akan terlihat dari luar perpustakaan / komponen? Kalau saja Base, kelas dasar abstrak normal ("antarmuka") dan implementasi konkret tanpa jerawat mungkin cukup.
D. Jurcau

@ D.Jurcau Kelas dasar dan turunan semuanya akan terlihat oleh publik. Jelas, kelas implementasi tidak akan.
Steve Emmerson

Mengapa tertunduk? Kelas dasar berada pada posisi yang aneh di sini, dapat diganti dengan pointer bersama dengan peningkatan keamanan jenis dan lebih sedikit kode.
Basilev

@ Basilevs saya tidak mengerti. Kelas dasar publik menggunakan idiom jerawat untuk menyembunyikan implementasi. Saya tidak melihat bagaimana menggantinya dengan pointer bersama dapat mempertahankan hierarki kelas tanpa membuang atau menggandakan pointer. Bisakah Anda memberikan contoh kode?
Steve Emmerson

Saya mengusulkan untuk menggandakan pointer, daripada mereplikasi downcast.
Basilev

Jawaban:


1

Saya pikir ini adalah strategi yang buruk untuk Derived_1::Impldiambil Base::Impl.

Tujuan utama menggunakan idiom Pimpl adalah untuk menyembunyikan detail implementasi kelas. Dengan membiarkan Derived_1::Implberasal Base::Impl, Anda telah mengalahkan tujuan itu. Sekarang, implementasi tidak hanya Basebergantung pada Base::Impl, implementasi Derived_1juga tergantung pada Base::Impl.

Apakah ada solusi yang lebih baik?

Itu tergantung pada trade-off apa yang dapat Anda terima.

Solusi 1

Jadikan Implkelas benar-benar independen. Ini akan menyiratkan bahwa akan ada dua petunjuk ke Implkelas - satu masuk Basedan satu lagi masuk 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;
};

Solusi 2

Ekspos kelas hanya sebagai pegangan. Jangan memaparkan definisi dan implementasi kelas sama sekali.

File header publik:

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

Inilah implementasi cepat

#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
}

Pro dan kontra

Dengan pendekatan pertama, Anda bisa membuat Derivedkelas di tumpukan. Dengan pendekatan kedua, itu bukan pilihan.

Dengan pendekatan pertama, Anda harus mengeluarkan biaya dua alokasi dinamis dan deallokasi untuk membangun dan merusak a Deriveddi stack. Jika Anda membangun dan merusak Derivedobjek dari heap Anda, dikenakan biaya satu alokasi lagi dan deallokasi. Dengan pendekatan kedua, Anda hanya dikenakan biaya satu alokasi dinamis dan satu alokasi untuk setiap objek.

Dengan pendekatan pertama, Anda mendapatkan kemampuan untuk menggunakan virtualfungsi anggota ini Base. Dengan pendekatan kedua, itu bukan pilihan.

Saran saya

Saya akan pergi dengan solusi pertama sehingga saya dapat menggunakan hirarki kelas dan virtualfungsi anggota Basemeskipun itu sedikit lebih mahal.


0

Satu-satunya perbaikan yang dapat saya lihat di sini adalah membiarkan kelas konkret menentukan bidang implementasi. Jika kelas dasar abstrak membutuhkannya, mereka dapat mendefinisikan properti abstrak yang mudah diterapkan di kelas konkret:

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

Ini tampaknya lebih aman bagi saya. Jika Anda memiliki pohon besar, Anda juga bisa memperkenalkannya virtual std::shared_ptr<Impl1> getImpl1() =0di tengah-tengah pohon.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.