หมายเหตุ: ต่อไปนี้คือรหัส C ++ 03 แต่เราคาดว่าจะย้ายไปที่ C ++ 11 ในอีกสองปีข้างหน้าดังนั้นเราต้องจำไว้
ฉันกำลังเขียนแนวทาง (สำหรับมือใหม่และคนอื่น ๆ ) เกี่ยวกับวิธีเขียนอินเทอร์เฟซแบบนามธรรมใน C ++ ฉันได้อ่านบทความของซัทเทอร์ทั้งสองเรื่องแล้วค้นหาตัวอย่างและคำตอบจากอินเทอร์เน็ตและทำการทดสอบ
รหัสนี้จะต้องไม่รวบรวม!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
พฤติกรรมทั้งหมดข้างต้นค้นหาต้นตอของปัญหาของพวกเขาในการแบ่ง : อินเทอร์เฟซแบบนามธรรม (หรือคลาสที่ไม่เป็นใบไม้ในลำดับชั้น) ไม่ควรสร้างได้หรือไม่สามารถปรับเปลี่ยนได้
โซลูชันที่ 0: อินเทอร์เฟซพื้นฐาน
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
วิธีแก้ปัญหานี้เรียบง่ายและไร้เดียงสา: มันไม่ผ่านข้อ จำกัด ทั้งหมดของเรา: สามารถสร้างได้โดยเริ่มต้นคัดลอกสร้างและคัดลอกกำหนด (ฉันไม่แน่ใจเกี่ยวกับการย้ายตัวสร้างและการมอบหมาย แต่ฉันยังคงคิด 2 ปี มันออกมา)
- เราไม่สามารถประกาศ destructor pure virtual ได้เนื่องจากเราต้องการเก็บไว้ในบรรทัดและคอมไพเลอร์บางส่วนของเราจะไม่แยกย่อยวิธีเสมือนจริงด้วย body ที่ว่างเปล่าแบบอินไลน์
- ใช่จุดเดียวของคลาสนี้คือทำให้ผู้ใช้งานสามารถทำลายได้จริงซึ่งเป็นกรณีที่เกิดขึ้นได้ยาก
- แม้ว่าเราจะมีวิธีการเสมือนจริงเพิ่มเติม (ซึ่งเป็นกรณีส่วนใหญ่) คลาสนี้จะยังคงสามารถคัดลอกได้
ดังนั้นไม่ ...
วิธีแก้ปัญหาที่ 1: เพิ่ม :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
โซลูชันนี้ดีที่สุดเนื่องจากเป็นธรรมดาชัดเจนและ C ++ (ไม่มีมาโคร)
ปัญหาคือมันยังคงไม่ทำงานสำหรับอินเทอร์เฟซเฉพาะนั้นเนื่องจาก VirtuallyConstructible ยังสามารถสร้างเป็นค่าเริ่มต้นได้
- เราไม่สามารถประกาศตัวทำลายล้างเสมือนจริงได้เพราะเราจำเป็นต้องเก็บอินไลน์ไว้และคอมไพเลอร์บางตัวของเราจะไม่แยกแยะ
- ใช่จุดเดียวของคลาสนี้คือทำให้ผู้ใช้งานสามารถทำลายได้จริงซึ่งเป็นกรณีที่เกิดขึ้นได้ยาก
ปัญหาอีกอย่างคือคลาสที่ใช้อินเตอร์เฟสที่ไม่สามารถคัดลอกได้จะต้องประกาศ / กำหนดตัวสร้างการคัดลอกและผู้ดำเนินการมอบหมายอย่างชัดเจนหากพวกเขาต้องการวิธีการเหล่านั้น (และในรหัสของเราเรามีคลาสค่าที่ลูกค้าของเรายังสามารถเข้าถึงได้ อินเตอร์เฟซ)
สิ่งนี้ขัดกับ Rule of Zero ซึ่งเป็นตำแหน่งที่เราต้องการไป: หากการใช้งานเริ่มต้นนั้นใช้ได้เราก็ควรจะสามารถใช้มันได้
วิธีที่ 2: ทำให้พวกเขาได้รับการคุ้มครอง!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
รูปแบบนี้เป็นไปตามข้อ จำกัด ทางเทคนิคที่เรามี (อย่างน้อยในรหัสผู้ใช้): MyInterface ไม่สามารถสร้างเป็นค่าเริ่มต้นไม่สามารถคัดลอกสร้างและไม่สามารถคัดลอกกำหนดได้
นอกจากนี้ยังไม่มีการกำหนดข้อ จำกัด ในการใช้คลาสซึ่งเป็นอิสระจากการปฏิบัติตามกฎของศูนย์หรือประกาศตัวสร้าง / ตัวดำเนินการสองสามตัวเป็น "= default" ใน C ++ 11/14 โดยไม่มีปัญหา
ทีนี้นี่ค่อนข้างละเอียดและมีทางเลือกอื่นที่จะใช้มาโครเช่น:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
การป้องกันต้องอยู่นอกแมโคร (เนื่องจากไม่มีขอบเขต)
"namespaced" อย่างถูกต้อง (นั่นคือนำหน้าด้วยชื่อของ บริษัท หรือผลิตภัณฑ์ของคุณ) แมโครควรไม่เป็นอันตราย
และข้อดีก็คือรหัสนั้นจะแยกตัวประกอบในแหล่งเดียวแทนที่จะคัดลอกวางในอินเทอร์เฟซทั้งหมด หากตัวสร้างการเคลื่อนย้ายและการกำหนดการย้ายถูกปิดการใช้งานอย่างชัดเจนเช่นเดียวกันในอนาคตนี่จะเป็นการเปลี่ยนแปลงเล็กน้อยในรหัส
ข้อสรุป
- ฉันหวาดระแวงที่ต้องการให้รหัสป้องกันการแบ่งส่วนข้อมูลในส่วนต่อประสานหรือไม่? (ฉันเชื่อว่าฉันไม่ได้ แต่ไม่มีใครรู้ ... )
- ทางออกที่ดีที่สุดในบรรดาข้างต้นคืออะไร?
- มีอีกวิธีที่ดีกว่า
โปรดจำไว้ว่านี่เป็นรูปแบบที่จะใช้เป็นแนวทางสำหรับมือใหม่ (ท่ามกลางคนอื่น ๆ ) ดังนั้นวิธีแก้ปัญหาเช่น: "แต่ละกรณีควรมีการนำไปใช้" ไม่ใช่วิธีแก้ปัญหาที่ใช้งานได้
รางวัลและผลลัพธ์
ฉันให้รางวัลแก่coredumpเพราะใช้เวลาในการตอบคำถามและความเกี่ยวข้องของคำตอบ
วิธีแก้ปัญหาของฉันอาจจะเป็นเช่นนั้น:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... ด้วยมาโครต่อไปนี้:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
นี่เป็นวิธีแก้ปัญหาที่ทำงานได้สำหรับปัญหาของฉันด้วยเหตุผลดังต่อไปนี้:
- คลาสนี้ไม่สามารถสร้างอินสแตนซ์ได้ (ตัวสร้างได้รับการป้องกัน)
- คลาสนี้สามารถถูกทำลายได้อย่างแท้จริง
- คลาสนี้สามารถสืบทอดโดยไม่กำหนดข้อ จำกัด ที่ไม่เหมาะสมในการสืบทอดคลาส (เช่นคลาสที่สืบทอดอาจเป็นโดยเริ่มต้น copiable)
- การใช้มาโครหมายถึงอินเตอร์เฟส "การประกาศ" สามารถจดจำได้ง่าย (และสามารถค้นหาได้) และรหัสของมันถูกแยกเป็นส่วน ๆ ในที่เดียวทำให้ง่ายต่อการแก้ไข (ชื่อที่นำหน้าอย่างเหมาะสมจะลบชื่อที่ไม่ต้องการ)
โปรดทราบว่าคำตอบอื่น ๆ ให้ข้อมูลเชิงลึกที่มีค่า ขอบคุณทุกคนที่ให้มันยิง
โปรดทราบว่าฉันเดาว่าฉันยังคงสามารถเพิ่มความโปรดปรานให้กับคำถามนี้ได้อีกและฉันให้ความสำคัญกับคำตอบที่เพียงพอที่ฉันจะเห็นได้ฉันจะเปิดเงินรางวัลเพื่อกำหนดให้กับคำตอบนั้น
virtual ~VirtuallyDestructible() = 0
และการสืบทอดคลาสอินเทอร์เฟซเสมือน (มีสมาชิกแบบนามธรรมเท่านั้น) คุณอาจละเว้นสิ่งที่ทำลายได้จริง
virtual void bar() = 0;
ตัวอย่างเช่น? ซึ่งจะป้องกันไม่ให้อินเทอร์เฟซของคุณถูกห้ามใช้งาน