ฉันควรใช้ C ++ private inheritance เมื่อใด


116

ซึ่งแตกต่างจากมรดกที่ได้รับการป้องกันการสืบทอดส่วนตัวของ C ++ พบวิธีในการพัฒนา C ++ กระแสหลัก อย่างไรก็ตามฉันยังไม่พบการใช้งานที่ดี

พวกคุณใช้มันเมื่อไหร่?

c++  oop 

คำตอบ:


60

หมายเหตุหลังการตอบรับ: นี่ไม่ใช่คำตอบที่สมบูรณ์ อ่านคำตอบอื่น ๆ เช่นที่นี่ (เชิงแนวคิด) และที่นี่ (ทั้งทางทฤษฎีและทางปฏิบัติ) หากคุณสนใจคำถาม นี่เป็นเพียงเคล็ดลับแฟนซีที่สามารถทำได้ด้วยมรดกส่วนตัว แม้ว่าจะเป็นเรื่องแปลกแต่ก็ไม่ใช่คำตอบสำหรับคำถาม

นอกเหนือจากการใช้งานพื้นฐานของการสืบทอดส่วนตัวที่แสดงในคำถามที่พบบ่อยของ C ++ (เชื่อมโยงในความคิดเห็นของผู้อื่น) คุณสามารถใช้การรวมกันของการสืบทอดส่วนตัวและเสมือนเพื่อปิดผนึกคลาส (ในคำศัพท์. NET) หรือเพื่อทำให้คลาสสุดท้าย (ในคำศัพท์ Java) . นี่ไม่ใช่การใช้งานทั่วไป แต่อย่างไรก็ตามฉันพบว่ามันน่าสนใจ:

class ClassSealer {
private:
   friend class Sealed;
   ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{ 
   // ...
};
class FailsToDerive : public Sealed
{
   // Cannot be instantiated
};

สามารถสร้างอินสแตนซ์ปิดผนึกได้ มาจากClassSealerและสามารถโทรหาผู้สร้างส่วนตัวได้โดยตรงเนื่องจากเป็นเพื่อน

FailsToDeriveจะไม่รวบรวมเป็นมันต้องเรียกClassSealerคอนสตรัคโดยตรง (ต้องการมรดกเสมือน) แต่มันก็ไม่สามารถที่มันเป็นเอกชนในSealedระดับและในกรณีนี้FailsToDeriveไม่ได้เป็นเพื่อนของClassSealer


แก้ไข

มีการกล่าวถึงในความคิดเห็นว่าสิ่งนี้ไม่สามารถทำให้เกิดขึ้นได้ทั่วไปในขณะที่ใช้ CRTP มาตรฐาน C ++ 11 จะลบข้อ จำกัด ดังกล่าวโดยจัดเตรียมไวยากรณ์ที่แตกต่างกันเพื่อผูกมิตรกับอาร์กิวเมนต์ของเทมเพลต:

template <typename T>
class Seal {
   friend T;          // not: friend class T!!!
   Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...

แน่นอนว่านี่คือการสงสัยทั้งหมดเนื่องจาก C ++ 11 มีfinalคำหลักตามบริบทเพื่อจุดประสงค์นี้:

class Sealed final // ...

นั่นเป็นเทคนิคที่ยอดเยี่ยม ฉันจะเขียนบล็อกเกี่ยวกับเรื่องนี้

1
คำถาม: ถ้าเราไม่ได้ใช้การสืบทอดเสมือน FailsToDerive จะคอมไพล์ แก้ไข?

4
+1 @ ซาช่า: จำเป็นต้องมีการสืบทอดเสมือนที่ถูกต้องเนื่องจากคลาสที่ได้รับส่วนใหญ่มักจะเรียกตัวสร้างของคลาสที่สืบทอดมาโดยตรงทั้งหมดซึ่งไม่ใช่กรณีที่มีการสืบทอดแบบธรรมดา
j_random_hacker

5
สิ่งนี้สามารถสร้างได้ทั่วไปโดยไม่ต้องสร้าง ClassSealer ที่กำหนดเองสำหรับทุกชั้นเรียนที่คุณต้องการปิดผนึก! ลองดู: คลาส ClassSealer {ป้องกัน: ClassSealer () {}}; นั่นคือทั้งหมด

+1 ไอราอิมบิลันจาเด็ดมาก! BTW ฉันเห็นความคิดเห็นก่อนหน้านี้ของคุณ (ตอนนี้ถูกลบไปแล้ว) เกี่ยวกับการใช้ CRTP: ฉันคิดว่าในความเป็นจริงมันควรจะใช้งานได้มันเป็นเรื่องยากที่จะได้รับไวยากรณ์สำหรับเพื่อนแม่แบบที่ถูกต้อง แต่ไม่ว่าในกรณีใดโซลูชันที่ไม่ใช่เทมเพลตของคุณจะยอดเยี่ยมกว่ามาก :)
j_random_hacker

138

ฉันจะใช้มันตลอดเวลา. ตัวอย่างบางส่วนจากด้านบนของหัวของฉัน:

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

ตัวอย่างทั่วไปได้มาจากคอนเทนเนอร์ STL แบบส่วนตัว:

class MyVector : private vector<int>
{
public:
    // Using declarations expose the few functions my clients need 
    // without a load of forwarding functions. 
    using vector<int>::push_back;
    // etc...  
};
  • เมื่อใช้รูปแบบอะแดปเตอร์การสืบทอดแบบส่วนตัวจากคลาส Adapted จะช่วยประหยัดการส่งต่อไปยังอินสแตนซ์ที่ปิด
  • เพื่อใช้งานอินเทอร์เฟซส่วนตัว สิ่งนี้มักเกิดขึ้นกับ Observer Pattern โดยปกติคลาส Observer ของฉัน MyClass จะบอกว่าสมัครตัวเองด้วย Subject บางอย่าง จากนั้นเฉพาะ MyClass เท่านั้นที่ต้องทำการแปลง MyClass -> Observer ส่วนที่เหลือของระบบไม่จำเป็นต้องรู้เกี่ยวกับเรื่องนี้ดังนั้นจึงมีการระบุการสืบทอดส่วนตัว

4
@Krsna: ที่จริงฉันไม่คิดอย่างนั้น มีเพียงเหตุผลเดียวที่นี่คือความเกียจคร้านนอกเหนือจากข้อสุดท้ายซึ่งน่าจะยุ่งยากกว่าในการหลีกเลี่ยง
Matthieu M.

11
ไม่ขี้เกียจมากนัก (เว้นแต่คุณจะหมายถึงในทางที่ดี) สิ่งนี้ช่วยให้สามารถสร้างฟังก์ชั่นโอเวอร์โหลดใหม่ที่ได้รับการเปิดเผยโดยไม่ต้องทำงานพิเศษใด ๆ หากใน C ++ 1x พวกเขาเพิ่ม 3 overloads ใหม่เพื่อpush_back, MyVectorได้รับพวกเขาได้ฟรี
David Stone

@DavidStone คุณไม่สามารถทำได้ด้วยวิธีการแม่แบบ?
Julien__

5
@Julien__: ใช่คุณสามารถเขียนหรือคุณอาจจะเขียนโดยใช้template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); } Base::f;หากคุณต้องการฟังก์ชันและความยืดหยุ่นส่วนใหญ่ที่มรดกส่วนตัวและusingคำสั่งมอบให้คุณคุณมีสัตว์ประหลาดตัวนั้นสำหรับแต่ละฟังก์ชั่น (และอย่าลืมเกี่ยวกับconstและvolatileโอเวอร์โหลด!)
David Stone

2
ฉันพูดว่าฟังก์ชันส่วนใหญ่เป็นเพราะคุณยังคงเรียกใช้ตัวสร้างการย้ายพิเศษหนึ่งตัวที่ไม่มีอยู่ในเวอร์ชันคำสั่งใช้ โดยทั่วไปคุณคาดหวังว่าสิ่งนี้จะได้รับการปรับให้เหมาะสมที่สุด แต่ในทางทฤษฎีฟังก์ชันอาจส่งคืนประเภทที่ไม่สามารถเคลื่อนย้ายได้ตามค่า เทมเพลตฟังก์ชันการส่งต่อยังมีการสร้างอินสแตนซ์เทมเพลตเพิ่มเติมและความลึกของ constexpr ซึ่งอาจทำให้โปรแกรมของคุณเข้าสู่ขีด จำกัด การใช้งาน
David Stone

31

การใช้มรดกส่วนตัวตามมาตรฐานคือความสัมพันธ์แบบ "นำไปใช้ในแง่ของ" (ขอบคุณ 'C ++ ที่มีประสิทธิภาพ' ของ Scott Meyers สำหรับถ้อยคำนี้) กล่าวอีกนัยหนึ่งอินเทอร์เฟซภายนอกของคลาสที่สืบทอดไม่มีความสัมพันธ์ (มองเห็นได้) กับคลาสที่สืบทอดมา แต่จะใช้อินเทอร์เฟซภายในเพื่อใช้ฟังก์ชันการทำงาน


6
อาจคุ้มค่าที่จะกล่าวถึงหนึ่งในเหตุผลที่ใช้ในกรณีนี้: สิ่งนี้ช่วยให้สามารถดำเนินการเพิ่มประสิทธิภาพคลาสฐานว่างได้ซึ่งจะไม่เกิดขึ้นหากคลาสนั้นเป็นสมาชิกแทนที่จะเป็นคลาสฐาน
jalf

2
การใช้งานหลักคือการลดการใช้พื้นที่ในกรณีที่มีความสำคัญเช่นในคลาสสตริงที่ควบคุมด้วยนโยบายหรือในคู่บีบอัด จริงๆแล้ว boost :: Compressed_pair ใช้มรดกที่มีการป้องกัน
Johannes Schaub - litb

jalf: เฮ้ฉันไม่รู้เรื่องนั้น ฉันคิดว่าการสืบทอดที่ไม่เปิดเผยต่อสาธารณะส่วนใหญ่จะใช้เป็นแฮ็กเมื่อคุณต้องการเข้าถึงสมาชิกที่ได้รับการคุ้มครองของชั้นเรียน ฉันสงสัยว่าทำไมวัตถุว่าง ๆ ถึงกินเนื้อที่เมื่อใช้การจัดองค์ประกอบ อาจเป็นไปได้สำหรับการระบุตำแหน่งที่เป็นสากล ...

3
นอกจากนี้ยังสะดวกในการทำให้ชั้นเรียนไม่สามารถคัดลอกได้ - เพียงแค่สืบทอดแบบส่วนตัวจากคลาสว่างเปล่าที่ไม่สามารถคัดลอกได้ ตอนนี้คุณไม่ต้องทำงานหนักในการประกาศ แต่ไม่ได้กำหนดตัวสร้างและตัวดำเนินการกำหนดสำเนาส่วนตัว เมเยอร์สพูดถึงเรื่องนี้ด้วย
Michael Burr

ฉันไม่ทราบว่าคำถามนี้เกี่ยวกับมรดกส่วนตัวแทนที่จะเป็นมรดกที่ได้รับการคุ้มครอง ใช่ฉันเดาว่ามีแอปพลิเคชั่นไม่น้อยสำหรับมัน ไม่สามารถนึกถึงตัวอย่างมากมายสำหรับมรดกที่ได้รับการคุ้มครองแม้ว่า: / ดูเหมือนว่าจะไม่ค่อยมีประโยชน์
Johannes Schaub - litb

23

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

ตัวอย่างเช่น:

class FooInterface
{
public:
    virtual void DoSomething() = 0;
};

class FooUser
{
public:
    bool RegisterFooInterface(FooInterface* aInterface);
};

class FooImplementer : private FooInterface
{
public:
    explicit FooImplementer(FooUser& aUser)
    {
        aUser.RegisterFooInterface(this);
    }
private:
    virtual void DoSomething() { ... }
};

ดังนั้นคลาส FooUser จึงสามารถเรียกใช้เมธอดส่วนตัวของ FooImplementer ผ่านอินเทอร์เฟซ FooInterface ในขณะที่คลาสภายนอกอื่น ๆ ไม่สามารถทำได้ นี่เป็นรูปแบบที่ยอดเยี่ยมสำหรับการจัดการการเรียกกลับเฉพาะที่กำหนดเป็นอินเทอร์เฟซ


1
อันที่จริงมรดกส่วนตัวเป็น IS-A ส่วนตัว
ซอกแซก

18

ฉันคิดว่าส่วนที่สำคัญจากC ++ FAQ Liteคือ:

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

หากมีข้อสงสัยคุณควรเลือกองค์ประกอบมากกว่ามรดกส่วนตัว


4

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

[แก้ไขในตัวอย่าง]

นำตัวอย่างที่เชื่อมโยงกับด้านบน พูดว่า

[... ] คลาส Wilma จำเป็นต้องเรียกใช้ฟังก์ชันสมาชิกจากคลาสใหม่ของคุณ Fred

คือจะบอกว่าวิลเฟร็ดเป็นที่ต้องการเพื่อให้สามารถเรียกใช้ฟังก์ชันสมาชิกบางอย่างหรือค่อนข้างจะบอกว่าวิลคือการติดต่อ ดังนั้นตามที่กล่าวไว้ในตัวอย่าง

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

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

[แก้ไขในตัวอย่างอื่น]

หน้านี้จะกล่าวถึงอินเทอร์เฟซส่วนตัวโดยสังเขป (จากอีกมุมหนึ่ง)


ฟังดูไม่มีประโยชน์จริงๆ ... โพสต์ตัวอย่างได้

ฉันคิดว่าฉันเข้าใจแล้วว่าคุณจะไปที่ไหน ... กรณีการใช้งานทั่วไปอาจเป็นไปได้ว่า Wilma เป็นคลาสยูทิลิตี้บางประเภทที่ต้องการเรียกใช้ฟังก์ชันเสมือนใน Fred แต่คลาสอื่น ๆ ไม่จำเป็นต้องรู้ว่า Fred ถูกนำไปใช้ในเงื่อนไข - ของ Wilma ขวา?
j_random_hacker

ใช่. ฉันควรชี้ให้เห็นว่าตามความเข้าใจของฉันคำว่า 'อินเทอร์เฟซ' มักใช้ใน Java มากกว่า เมื่อฉันได้ยินครั้งแรกฉันคิดว่ามันน่าจะเป็นชื่อที่ดีกว่านี้ เนื่องจากในตัวอย่างนี้เรามีอินเทอร์เฟซที่ไม่มีใครเชื่อมต่อในแบบที่เรานึกถึงคำนี้ตามปกติ
อคติ

@Noos: ใช่ฉันคิดว่าคำพูดของคุณ "Wilma เป็นอินเทอร์เฟซ" ค่อนข้างคลุมเครือเนื่องจากคนส่วนใหญ่มักจะคิดว่า Wilma เป็นอินเทอร์เฟซที่ Fred ตั้งใจจะจัดหาให้กับโลกแทนที่จะเป็นสัญญากับ Wilma เท่านั้น
j_random_hacker

@j_ นั่นเป็นเหตุผลที่ฉันคิดว่าอินเทอร์เฟซเป็นชื่อที่ไม่ดี อินเทอร์เฟซคำนี้ไม่จำเป็นต้องมีความหมายต่อโลกอย่างที่คิด แต่เป็นการรับประกันการใช้งาน อันที่จริงฉันกำลังถกเถียงกันอยู่เกี่ยวกับอินเทอร์เฟซคำศัพท์ในคลาส Program Design ของฉัน แต่เราใช้สิ่งที่เราได้รับ ...
อคติ

2

บางครั้งฉันพบว่ามีประโยชน์ในการใช้การสืบทอดส่วนตัวเมื่อฉันต้องการเปิดเผยอินเทอร์เฟซที่เล็กกว่า (เช่นคอลเลกชัน) ในอินเทอร์เฟซของอินเทอร์เฟซอื่นซึ่งการใช้งานคอลเล็กชันต้องการการเข้าถึงสถานะของคลาสการเปิดเผยในลักษณะที่คล้ายกับคลาสภายในใน ชวา

class BigClass;

struct SomeCollection
{
    iterator begin();
    iterator end();
};

class BigClass : private SomeCollection
{
    friend struct SomeCollection;
    SomeCollection &GetThings() { return *this; }
};

แล้วถ้า SomeCollection ความต้องการที่จะเข้าถึง BigClass static_cast<BigClass *>(this)ก็สามารถ ไม่จำเป็นต้องมีสมาชิกข้อมูลเพิ่มเติมที่กินพื้นที่


ไม่จำเป็นต้องมีการประกาศล่วงหน้าBigClassในตัวอย่างนี้หรือไม่? ฉันคิดว่าสิ่งนี้น่าสนใจ แต่มันกรีดร้องอย่างแฮ็กที่ใบหน้าของฉัน
Thomas Eding

2

ฉันพบแอปพลิเคชั่นที่ดีสำหรับการสืบทอดส่วนตัวแม้ว่าจะมีการใช้งานที่ จำกัด

ปัญหาในการแก้ไข

สมมติว่าคุณได้รับ C API ต่อไปนี้:

#ifdef __cplusplus
extern "C" {
#endif

    typedef struct
    {
        /* raw owning pointer, it's C after all */
        char const * name;

        /* more variables that need resources
         * ...
         */
    } Widget;

    Widget const * loadWidget();

    void freeWidget(Widget const * widget);

#ifdef __cplusplus
} // end of extern "C"
#endif

ตอนนี้งานของคุณคือใช้ API นี้โดยใช้ C ++

แนวทาง C-ish

แน่นอนว่าเราสามารถเลือกรูปแบบการใช้งาน C-ish ได้ดังนี้:

Widget const * loadWidget()
{
    auto result = std::make_unique<Widget>();
    result->name = strdup("The Widget name");
    // More similar assignments here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    free(result->name);
    // More similar manual freeing of resources
    delete widget;
}

แต่มีข้อเสียหลายประการ:

  • การจัดการทรัพยากรด้วยตนเอง (เช่นหน่วยความจำ)
  • เป็นเรื่องง่ายที่จะตั้งค่าstructผิด
  • เป็นเรื่องง่ายที่จะลืมการปลดปล่อยทรัพยากรเมื่อปล่อยไฟล์ struct
  • มันคือ C-ish

แนวทาง C ++

เราได้รับอนุญาตให้ใช้ C ++ แล้วทำไมไม่ใช้พลังเต็มที่ล่ะ?

ขอแนะนำการจัดการทรัพยากรอัตโนมัติ

ปัญหาข้างต้นโดยพื้นฐานแล้วทั้งหมดเกี่ยวข้องกับการจัดการทรัพยากรด้วยตนเอง วิธีแก้ปัญหาที่อยู่ในใจคือการสืบทอดจากWidgetและเพิ่มอินสแตนซ์การจัดการทรัพยากรไปยังคลาสที่ได้รับWidgetImplสำหรับแต่ละตัวแปร:

class WidgetImpl : public Widget
{
public:
    // Added bonus, Widget's members get default initialized
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

private:
    std::string m_nameResource;
};

สิ่งนี้ช่วยลดความยุ่งยากในการนำไปใช้ดังต่อไปนี้:

Widget const * loadWidget()
{
    auto result = std::make_unique<WidgetImpl>();
    result->setName("The Widget name");
    // More similar setters here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    // No virtual destructor in the base class, thus static_cast must be used
    delete static_cast<WidgetImpl const *>(widget);
}

เช่นนี้เราได้แก้ไขปัญหาข้างต้นทั้งหมด แต่ลูกค้ายังคงลืมเกี่ยวกับผู้กำหนดWidgetImplและมอบหมายให้กับWidgetสมาชิกได้โดยตรง

มรดกส่วนตัวเข้าสู่ขั้นตอน

ในการห่อหุ้มWidgetสมาชิกเราใช้มรดกส่วนตัว น่าเสียดายที่ตอนนี้เราต้องการฟังก์ชั่นพิเศษสองอย่างในการส่งระหว่างทั้งสองคลาส:

class WidgetImpl : private Widget
{
public:
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

    Widget const * toWidget() const
    {
        return static_cast<Widget const *>(this);
    }

    static void deleteWidget(Widget const * const widget)
    {
        delete static_cast<WidgetImpl const *>(widget);
    }

private:
    std::string m_nameResource;
};

สิ่งนี้ทำให้จำเป็นต้องดัดแปลงต่อไปนี้:

Widget const * loadWidget()
{
    auto widgetImpl = std::make_unique<WidgetImpl>();
    widgetImpl->setName("The Widget name");
    // More similar setters here
    auto const result = widgetImpl->toWidget();
    widgetImpl.release();
    return result;
}

void freeWidget(Widget const * const widget)
{
    WidgetImpl::deleteWidget(widget);
}

โซลูชันนี้ช่วยแก้ปัญหาทั้งหมดได้ ไม่มีการจัดการหน่วยความจำด้วยตนเองและWidgetมีการห่อหุ้มอย่างดีเพื่อให้WidgetImplไม่มีข้อมูลสาธารณะอีกต่อไป ทำให้การนำไปใช้งานง่ายอย่างถูกต้องและยาก (เป็นไปไม่ได้?) ที่จะใช้ผิด

ข้อมูลโค้ดรูปแบบเช่นการรวบรวมใน Coliru


1

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

จากนั้นคุณควรใช้การสืบทอดส่วนตัวมิฉะนั้นคุณจะมีอันตรายจากวิธีการปลดล็อคพื้นฐานที่ส่งออกผ่านคลาสที่ได้รับนี้


1

บางครั้งอาจเป็นอีกทางเลือกหนึ่งในการรวมตัวอย่างเช่นหากคุณต้องการการรวม แต่มีพฤติกรรมที่เปลี่ยนแปลงไปของเอนทิตีที่รวมได้ (การแทนที่ฟังก์ชันเสมือน)

แต่คุณพูดถูกมันมีตัวอย่างจากโลกแห่งความจริงไม่มากนัก


0

เพียงเพราะ C ++ มีคุณสมบัติไม่ได้หมายความว่ามันมีประโยชน์หรือควรใช้

ฉันบอกว่าคุณไม่ควรใช้มันเลย

หากคุณกำลังใช้มันอยู่ดีแสดงว่าคุณกำลังละเมิดการห่อหุ้มและลดการทำงานร่วมกัน คุณกำลังใส่ข้อมูลในคลาสหนึ่งและเพิ่มเมธอดที่จัดการข้อมูลในคลาสอื่น

เช่นเดียวกับคุณสมบัติ C ++ อื่น ๆ สามารถใช้เพื่อให้ได้ผลข้างเคียงเช่นการปิดผนึกคลาส (ตามที่กล่าวไว้ในคำตอบของ dribeas) แต่สิ่งนี้ไม่ได้ทำให้เป็นคุณสมบัติที่ดี


คุณกำลังประชดประชัน? ทั้งหมดที่ฉันมีคือ -1! อย่างไรก็ตามฉันจะไม่ลบสิ่งนี้แม้ว่าจะได้รับคะแนนโหวต -100
hasen

9
" โดยพื้นฐานแล้วคุณกำลังละเมิดการห่อหุ้ม " คุณช่วยยกตัวอย่างได้ไหม
ซอกแซก

1
ข้อมูลในระดับหนึ่งและพฤติกรรมในเสียงอื่นเช่นการเพิ่มขึ้นของความยืดหยุ่นเนื่องจากอาจมีระดับพฤติกรรมมากกว่าหนึ่งและลูกค้าและเลือกหนึ่งที่พวกเขาต้องการที่จะตอบสนองสิ่งที่พวกเขาต้องการ
Makar

0

Private Inheritance ที่จะใช้เมื่อรีเลชันไม่ใช่ "เป็น" แต่คลาสใหม่สามารถ "ใช้งานในรูปแบบของคลาสที่มีอยู่" หรือคลาสใหม่ "ทำงานเหมือน" คลาสที่มีอยู่

ตัวอย่างจาก "มาตรฐานการเข้ารหัส C ++ โดย Andrei Alexandrescu, Herb Sutter": - พิจารณาว่า Square และ Rectangle ทั้ง 2 คลาสมีฟังก์ชันเสมือนสำหรับกำหนดความสูงและความกว้าง จากนั้น Square ไม่สามารถสืบทอดจาก Rectangle ได้อย่างถูกต้องเนื่องจากโค้ดที่ใช้ Rectangle ที่ปรับเปลี่ยนได้จะถือว่า SetWidth ไม่เปลี่ยนความสูง (ไม่ว่า Rectangle จะจัดทำเอกสารสัญญานั้นอย่างชัดเจนหรือไม่ก็ตาม) ในขณะที่ Square :: SetWidth ไม่สามารถรักษาสัญญานั้นและความเหลี่ยมของตัวเองไม่แปรผันที่ ในเวลาเดียวกัน. แต่ Rectangle ไม่สามารถสืบทอดจาก Square ได้อย่างถูกต้องเช่นกันหากลูกค้าของ Square สมมติว่าพื้นที่ของ Square คือความกว้างกำลังสองหรือถ้าพวกเขาอาศัยคุณสมบัติอื่น ๆ ที่ไม่ได้ยึดไว้สำหรับ Rectangles

สี่เหลี่ยมจัตุรัส "is-a" (ในทางคณิตศาสตร์) แต่สี่เหลี่ยมจัตุรัสไม่ใช่สี่เหลี่ยมผืนผ้า (ตามพฤติกรรม) ดังนั้นแทนที่จะเป็น "is-a" เราจึงชอบที่จะพูดว่า "works-like-a" (หรือถ้าคุณต้องการ "usable-as-a") เพื่อให้คำอธิบายมีแนวโน้มที่จะเข้าใจผิดน้อยลง


0

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

struct QuadraticEquationState {
   const double a;
   const double b;
   const double c;

   // named ctors so aggregate construction is available,
   // which is the default usage pattern
   // add your favourite ctors - throwing, try, cps
   static QuadraticEquationState read(std::istream& is);
   static std::optional<QuadraticEquationState> try_read(std::istream& is);

   template<typename Then, typename Else>
   static std::common_type<
             decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
             decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
   if_read(std::istream& is, Then then, Else els);
};

// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);

// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);

struct QuadraticEquationCache {
   mutable std::optional<double> determinant_cache;
   mutable std::optional<double> x1_cache;
   mutable std::optional<double> x2_cache;
   mutable std::optional<double> sum_of_x12_cache;
};

class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
                          private QuadraticEquationCache {
public:
   QuadraticEquation(QuadraticEquationState); // in general, might throw
   QuadraticEquation(const double a, const double b, const double c);
   QuadraticEquation(const std::string& str);
   QuadraticEquation(const ExpressionTree& str); // might throw
}

ณ จุดนี้คุณอาจเก็บคอลเลกชันของแคชไว้ในคอนเทนเนอร์และค้นหาการสร้าง สะดวกหากมีการประมวลผลจริง โปรดทราบว่าแคชเป็นส่วนหนึ่งของ QE: การดำเนินการที่กำหนดบน QE อาจหมายความว่าแคชสามารถใช้ซ้ำได้บางส่วน (เช่น c ไม่มีผลต่อผลรวม) แต่เมื่อไม่มีแคชคุณควรค้นหา

มรดกส่วนตัวสามารถจำลองโดยสมาชิกได้เกือบตลอดเวลา (จัดเก็บข้อมูลอ้างอิงไปยังฐานหากจำเป็น) การสร้างแบบจำลองแบบนั้นไม่คุ้มค่าเสมอไป บางครั้งการถ่ายทอดทางพันธุกรรมเป็นการแสดงที่มีประสิทธิภาพมากที่สุด


0

หากคุณต้องการstd::ostreamเปลี่ยนแปลงเล็กน้อย (เช่นในคำถามนี้ ) คุณอาจต้องทำ

  1. สร้างคลาสMyStreambufที่มาจากstd::streambufและใช้การเปลี่ยนแปลงที่นั่น
  2. สร้างคลาสMyOStreamที่ได้มาจากการstd::ostreamเริ่มต้นและจัดการอินสแตนซ์ของMyStreambufและส่งตัวชี้ไปยังอินสแตนซ์นั้นไปยังตัวสร้างของstd::ostream

แนวคิดแรกอาจเป็นการเพิ่มMyStreamอินสแตนซ์เป็นสมาชิกข้อมูลในMyOStreamคลาส:

class MyOStream : public std::ostream
{
public:
    MyOStream()
        : std::basic_ostream{ &m_buf }
        , m_buf{}
    {}

private:
    MyStreambuf m_buf;
};

แต่คลาสพื้นฐานจะสร้างขึ้นก่อนสมาชิกข้อมูลใด ๆ ดังนั้นคุณจึงส่งตัวชี้ไปยังstd::streambufอินสแตนซ์ที่ยังไม่ได้สร้างstd::ostreamซึ่งเป็นพฤติกรรมที่ไม่ได้กำหนด

วิธีแก้ปัญหาเสนอไว้ในคำตอบของ Ben สำหรับคำถามข้างต้นเพียงแค่สืบทอดจากบัฟเฟอร์สตรีมก่อนจากนั้นจึงเริ่มต้นสตรีมด้วยthis:

class MyOStream : public MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

อย่างไรก็ตามคลาสผลลัพธ์ยังสามารถใช้เป็นstd::streambufอินสแตนซ์ซึ่งมักจะไม่ต้องการ การเปลี่ยนไปใช้มรดกส่วนตัวช่วยแก้ปัญหานี้ได้:

class MyOStream : private MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.