นี่เป็นวิธีที่ดีสำหรับลำดับชั้นของคลาส“ pImpl” ใน C ++ หรือไม่


9

ฉันมีลำดับชั้นเรียนที่ฉันต้องการแยกอินเทอร์เฟซจากการใช้งาน โซลูชันของฉันคือการมีสองลำดับชั้น: ลำดับชั้นของการจัดการสำหรับอินเทอร์เฟซและลำดับชั้นของคลาสที่ไม่เป็นสาธารณะสำหรับการนำไปใช้ คลาสตัวจัดการพื้นฐานมีตัวชี้การใช้งานซึ่งคลาสตัวจัดการที่ได้รับมาส่งไปยังตัวชี้ของชนิดที่ได้รับ (ดูฟังก์ชันgetPimpl())

นี่คือภาพร่างของโซลูชันของฉันสำหรับคลาสพื้นฐานที่มีสองคลาสที่ได้รับ มีวิธีแก้ปัญหาที่ดีกว่านี้ไหม?

ไฟล์ "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;
    ...
};

ไฟล์ "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(...); }

คลาสใดที่จะมองเห็นได้จากด้านนอกของไลบรารี / คอมโพเนนต์ ถ้าเท่านั้นBaseคลาสฐานนามธรรมปกติ ("อินเตอร์เฟส") และการใช้งานที่เป็นรูปธรรมโดยไม่มี pimpl อาจเพียงพอ
D. Jurcau

@ D.Jurcau คลาสพื้นฐานและคลาสที่ได้รับจะปรากฏต่อสาธารณะ เห็นได้ชัดว่าการเรียนการใช้งานจะไม่
Steve Emmerson

ทำไมต้องเศร้าใจ คลาสฐานอยู่ในตำแหน่งแปลก ๆ ที่นี่สามารถแทนที่ด้วยตัวชี้ที่ใช้ร่วมกันที่มีการปรับปรุงประเภทความปลอดภัยและรหัสน้อย
Basilevs

@ Basilevs ฉันไม่เข้าใจ คลาสฐานสาธารณะใช้ pimpl สำนวนเพื่อซ่อนการใช้งาน ฉันไม่เห็นวิธีแทนที่ด้วยตัวชี้ที่ใช้ร่วมกันสามารถรักษาลำดับชั้นของคลาสโดยไม่ต้องชี้ขาดหรือทำซ้ำตัวชี้ คุณสามารถให้ตัวอย่างรหัสได้หรือไม่
Steve Emmerson

ฉันเสนอให้พอยน์เตอร์ซ้ำกันแทนที่จะทำซ้ำ downcast
Basilevs

คำตอบ:


1

ฉันคิดว่ามันเป็นกลยุทธ์ที่ไม่ดีที่จะทำให้เป็นผลมาจากDerived_1::ImplBase::Impl

วัตถุประสงค์หลักของการใช้ Pimpl สำนวนคือการซ่อนรายละเอียดการใช้งานของชั้นเรียน เมื่อปล่อยให้Derived_1::Implเป็นBase::Implเช่นนั้นคุณจะต้องเสียจุดประสงค์นั้น ขณะนี้ไม่เพียง แต่การดำเนินการBaseขึ้นอยู่กับBase::Implการดำเนินงานของยังขึ้นอยู่กับDerived_1Base::Impl

มีวิธีแก้ปัญหาที่ดีกว่านี้ไหม?

ขึ้นอยู่กับการยอมรับของคุณ

โซลูชันที่ 1

ทำให้Implชั้นเรียนเป็นอิสระโดยสิ้นเชิง นี้จะบ่งบอกว่าจะมีสองตัวชี้ไปยังImplชั้นเรียน - หนึ่งในและอีกคนหนึ่งในBaseDerived_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;
};

โซลูชันที่ 2

เปิดเผยคลาสเป็นตัวจัดการเท่านั้น อย่าเปิดเผยคำจำกัดความของคลาสและการนำไปใช้เลย

ไฟล์ส่วนหัวสาธารณะ:

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

นี่คือการใช้งานที่รวดเร็ว

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

ข้อดีและข้อเสีย

ด้วยวิธีแรกคุณสามารถสร้างDerivedคลาสในสแต็ก ด้วยวิธีที่สองนั่นไม่ใช่ตัวเลือก

ด้วยวิธีแรกคุณต้องเสียค่าใช้จ่ายในการจัดสรรแบบไดนามิกและการจัดสรรคืนสองครั้งสำหรับการสร้างและการทำลายDerivedในสแต็ก หากคุณสร้างและทำลายDerivedวัตถุจากฮีปคุณต้องเสียค่าใช้จ่ายในการจัดสรรและการจัดสรรคืนเพิ่มอีกหนึ่งรายการ ด้วยวิธีที่สองคุณจะต้องเสียค่าใช้จ่ายในการจัดสรรแบบไดนามิกและการจัดสรรคืนหนึ่งครั้งสำหรับทุกวัตถุ

ด้วยวิธีแรกคุณจะได้รับมีความสามารถในการใช้งานฟังก์ชั่นสมาชิกvirtual Baseด้วยวิธีที่สองนั่นไม่ใช่ตัวเลือก

คำแนะนำของฉัน

ฉันจะใช้วิธีแก้ปัญหาแรกเพื่อให้ฉันสามารถใช้ลำดับชั้นของชั้นเรียนและvirtualฟังก์ชั่นสมาชิกBaseได้แม้ว่ามันจะแพงกว่านิดหน่อย


0

การปรับปรุงเพียงอย่างเดียวที่ฉันเห็นที่นี่คือเพื่อให้คลาสคอนกรีตกำหนดฟิลด์การใช้งาน หากคลาสฐาน abstract ต้องการพวกเขาสามารถกำหนดคุณสมบัติ abstract ที่ใช้งานง่ายในคลาสคอนกรีต:

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

ดูเหมือนว่าจะปลอดภัยกว่าสำหรับฉัน หากคุณมีต้นไม้ใหญ่คุณสามารถแนะนำvirtual std::shared_ptr<Impl1> getImpl1() =0ที่กลางต้นไม้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.