ซึ่งแตกต่างจากมรดกที่ได้รับการป้องกันการสืบทอดส่วนตัวของ C ++ พบวิธีในการพัฒนา C ++ กระแสหลัก อย่างไรก็ตามฉันยังไม่พบการใช้งานที่ดี
พวกคุณใช้มันเมื่อไหร่?
ซึ่งแตกต่างจากมรดกที่ได้รับการป้องกันการสืบทอดส่วนตัวของ C ++ พบวิธีในการพัฒนา C ++ กระแสหลัก อย่างไรก็ตามฉันยังไม่พบการใช้งานที่ดี
พวกคุณใช้มันเมื่อไหร่?
คำตอบ:
หมายเหตุหลังการตอบรับ: นี่ไม่ใช่คำตอบที่สมบูรณ์ อ่านคำตอบอื่น ๆ เช่นที่นี่ (เชิงแนวคิด) และที่นี่ (ทั้งทางทฤษฎีและทางปฏิบัติ) หากคุณสนใจคำถาม นี่เป็นเพียงเคล็ดลับแฟนซีที่สามารถทำได้ด้วยมรดกส่วนตัว แม้ว่าจะเป็นเรื่องแปลกแต่ก็ไม่ใช่คำตอบสำหรับคำถาม
นอกเหนือจากการใช้งานพื้นฐานของการสืบทอดส่วนตัวที่แสดงในคำถามที่พบบ่อยของ 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 // ...
ฉันจะใช้มันตลอดเวลา. ตัวอย่างบางส่วนจากด้านบนของหัวของฉัน:
ตัวอย่างทั่วไปได้มาจากคอนเทนเนอร์ 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...
};
push_back
, MyVector
ได้รับพวกเขาได้ฟรี
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
โอเวอร์โหลด!)
การใช้มรดกส่วนตัวตามมาตรฐานคือความสัมพันธ์แบบ "นำไปใช้ในแง่ของ" (ขอบคุณ 'C ++ ที่มีประสิทธิภาพ' ของ Scott Meyers สำหรับถ้อยคำนี้) กล่าวอีกนัยหนึ่งอินเทอร์เฟซภายนอกของคลาสที่สืบทอดไม่มีความสัมพันธ์ (มองเห็นได้) กับคลาสที่สืบทอดมา แต่จะใช้อินเทอร์เฟซภายในเพื่อใช้ฟังก์ชันการทำงาน
การใช้การสืบทอดส่วนตัวที่เป็นประโยชน์อย่างหนึ่งคือเมื่อคุณมีคลาสที่ใช้อินเทอร์เฟซซึ่งจะถูกลงทะเบียนกับอ็อบเจ็กต์อื่น คุณทำให้อินเทอร์เฟซนั้นเป็นแบบส่วนตัวเพื่อให้คลาสนั้น ๆ ต้องลงทะเบียนและมีเพียงอ็อบเจ็กต์เฉพาะที่ลงทะเบียนด้วยเท่านั้นที่สามารถใช้ฟังก์ชันเหล่านั้นได้
ตัวอย่างเช่น:
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 ในขณะที่คลาสภายนอกอื่น ๆ ไม่สามารถทำได้ นี่เป็นรูปแบบที่ยอดเยี่ยมสำหรับการจัดการการเรียกกลับเฉพาะที่กำหนดเป็นอินเทอร์เฟซ
ฉันคิดว่าส่วนที่สำคัญจากC ++ FAQ Liteคือ:
การใช้งานในระยะยาวที่ถูกต้องตามกฎหมายสำหรับการสืบทอดส่วนตัวคือเมื่อคุณต้องการสร้างคลาส Fred ที่ใช้รหัสในคลาส Wilma และรหัสจากคลาส Wilma จำเป็นต้องเรียกใช้ฟังก์ชันสมาชิกจากคลาสใหม่ของคุณ Fred ในกรณีนี้ Fred เรียกสิ่งที่ไม่ใช่เสมือนใน Wilma และ Wilma เรียก (โดยปกติจะเป็นเสมือนจริง) ในตัวมันเองซึ่ง Fred จะถูกแทนที่ การจัดองค์ประกอบภาพจะยากกว่ามาก
หากมีข้อสงสัยคุณควรเลือกองค์ประกอบมากกว่ามรดกส่วนตัว
ฉันคิดว่ามีประโยชน์สำหรับอินเทอร์เฟซ (ได้แก่ คลาสนามธรรม) ที่ฉันกำลังสืบทอดโดยที่ฉันไม่ต้องการให้โค้ดอื่นสัมผัสอินเทอร์เฟซ (เฉพาะคลาสที่สืบทอด)
[แก้ไขในตัวอย่าง]
นำตัวอย่างที่เชื่อมโยงกับด้านบน พูดว่า
[... ] คลาส Wilma จำเป็นต้องเรียกใช้ฟังก์ชันสมาชิกจากคลาสใหม่ของคุณ Fred
คือจะบอกว่าวิลเฟร็ดเป็นที่ต้องการเพื่อให้สามารถเรียกใช้ฟังก์ชันสมาชิกบางอย่างหรือค่อนข้างจะบอกว่าวิลคือการติดต่อ ดังนั้นตามที่กล่าวไว้ในตัวอย่าง
มรดกส่วนตัวไม่ใช่สิ่งชั่วร้าย มันแพงกว่าในการดูแลรักษาเนื่องจากจะเพิ่มความเป็นไปได้ที่จะมีคนเปลี่ยนบางอย่างที่จะทำลายรหัสของคุณ
ความคิดเห็นเกี่ยวกับเอฟเฟกต์ที่ต้องการของโปรแกรมเมอร์ที่ต้องตรงตามข้อกำหนดอินเทอร์เฟซของเราหรือทำลายโค้ด และเนื่องจาก fredCallsWilma () ได้รับการปกป้องเฉพาะเพื่อนและคลาสที่ได้รับเท่านั้นที่สามารถสัมผัสได้เช่นอินเทอร์เฟซที่สืบทอดมา (คลาสนามธรรม) ที่มีเพียงคลาสที่สืบทอดเท่านั้นที่สามารถสัมผัสได้ (และเพื่อน)
[แก้ไขในตัวอย่างอื่น]
หน้านี้จะกล่าวถึงอินเทอร์เฟซส่วนตัวโดยสังเขป (จากอีกมุมหนึ่ง)
บางครั้งฉันพบว่ามีประโยชน์ในการใช้การสืบทอดส่วนตัวเมื่อฉันต้องการเปิดเผยอินเทอร์เฟซที่เล็กกว่า (เช่นคอลเลกชัน) ในอินเทอร์เฟซของอินเทอร์เฟซอื่นซึ่งการใช้งานคอลเล็กชันต้องการการเข้าถึงสถานะของคลาสการเปิดเผยในลักษณะที่คล้ายกับคลาสภายในใน ชวา
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
ในตัวอย่างนี้หรือไม่? ฉันคิดว่าสิ่งนี้น่าสนใจ แต่มันกรีดร้องอย่างแฮ็กที่ใบหน้าของฉัน
ฉันพบแอปพลิเคชั่นที่ดีสำหรับการสืบทอดส่วนตัวแม้ว่าจะมีการใช้งานที่ จำกัด
สมมติว่าคุณได้รับ 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 ได้ดังนี้:
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 ++ แล้วทำไมไม่ใช้พลังเต็มที่ล่ะ?
ปัญหาข้างต้นโดยพื้นฐานแล้วทั้งหมดเกี่ยวข้องกับการจัดการทรัพยากรด้วยตนเอง วิธีแก้ปัญหาที่อยู่ในใจคือการสืบทอดจาก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
หากคลาสที่ได้รับ - จำเป็นต้องใช้โค้ดซ้ำและ - คุณไม่สามารถเปลี่ยนคลาสพื้นฐานได้และ - กำลังปกป้องวิธีการของมันโดยใช้สมาชิกของฐานภายใต้การล็อค
จากนั้นคุณควรใช้การสืบทอดส่วนตัวมิฉะนั้นคุณจะมีอันตรายจากวิธีการปลดล็อคพื้นฐานที่ส่งออกผ่านคลาสที่ได้รับนี้
บางครั้งอาจเป็นอีกทางเลือกหนึ่งในการรวมตัวอย่างเช่นหากคุณต้องการการรวม แต่มีพฤติกรรมที่เปลี่ยนแปลงไปของเอนทิตีที่รวมได้ (การแทนที่ฟังก์ชันเสมือน)
แต่คุณพูดถูกมันมีตัวอย่างจากโลกแห่งความจริงไม่มากนัก
เพียงเพราะ C ++ มีคุณสมบัติไม่ได้หมายความว่ามันมีประโยชน์หรือควรใช้
ฉันบอกว่าคุณไม่ควรใช้มันเลย
หากคุณกำลังใช้มันอยู่ดีแสดงว่าคุณกำลังละเมิดการห่อหุ้มและลดการทำงานร่วมกัน คุณกำลังใส่ข้อมูลในคลาสหนึ่งและเพิ่มเมธอดที่จัดการข้อมูลในคลาสอื่น
เช่นเดียวกับคุณสมบัติ C ++ อื่น ๆ สามารถใช้เพื่อให้ได้ผลข้างเคียงเช่นการปิดผนึกคลาส (ตามที่กล่าวไว้ในคำตอบของ dribeas) แต่สิ่งนี้ไม่ได้ทำให้เป็นคุณสมบัติที่ดี
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") เพื่อให้คำอธิบายมีแนวโน้มที่จะเข้าใจผิดน้อยลง
คลาสมีค่าคงที่ ค่าคงที่ถูกกำหนดโดยตัวสร้าง อย่างไรก็ตามในหลาย ๆ สถานการณ์การมีมุมมองสถานะการเป็นตัวแทนของวัตถุมีประโยชน์ (ซึ่งคุณสามารถส่งผ่านเครือข่ายหรือบันทึกลงในไฟล์ - 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 ไม่มีผลต่อผลรวม) แต่เมื่อไม่มีแคชคุณควรค้นหา
มรดกส่วนตัวสามารถจำลองโดยสมาชิกได้เกือบตลอดเวลา (จัดเก็บข้อมูลอ้างอิงไปยังฐานหากจำเป็น) การสร้างแบบจำลองแบบนั้นไม่คุ้มค่าเสมอไป บางครั้งการถ่ายทอดทางพันธุกรรมเป็นการแสดงที่มีประสิทธิภาพมากที่สุด
หากคุณต้องการstd::ostream
เปลี่ยนแปลงเล็กน้อย (เช่นในคำถามนี้ ) คุณอาจต้องทำ
MyStreambuf
ที่มาจากstd::streambuf
และใช้การเปลี่ยนแปลงที่นั่น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 }
{}
};