ทำไมต้องตอบอีก?
หลาย ๆ โพสต์เกี่ยวกับ SO และบทความภายนอกกล่าวว่าปัญหาเพชรนั้นได้รับการแก้ไขโดยการสร้างอินสแตนซ์เดียวA
แทนที่จะเป็นสองอินสแตนซ์(หนึ่งอันสำหรับพาเรนต์แต่ละตัวD
) ซึ่งจะช่วยแก้ไขความคลุมเครือ อย่างไรก็ตามสิ่งนี้ไม่ได้ช่วยให้ฉันเข้าใจกระบวนการได้อย่างครอบคลุมฉันจบลงด้วยคำถามเพิ่มเติมเช่น
- จะเกิดอะไรขึ้นถ้า
B
และC
พยายามสร้างอินสแตนซ์ที่แตกต่างกันA
เช่นการเรียกตัวสร้างพารามิเตอร์ที่มีพารามิเตอร์ต่างกัน ( D::D(int x, int y): C(x), B(y) {}
)? ซึ่งตัวอย่างของการA
จะได้รับเลือกให้เป็นส่วนหนึ่งของD
?
- จะเกิดอะไรขึ้นถ้าฉันใช้การสืบทอดที่ไม่ใช่เสมือน
B
แต่เป็นเสมือนสำหรับC
? มันก็เพียงพอสำหรับการสร้างเช่นเดียวของA
ในD
?
- ฉันควรใช้การสืบทอดเสมือนโดยค่าเริ่มต้นนับจากนี้ไปเป็นมาตรการป้องกันเนื่องจากสามารถแก้ปัญหาเพชรที่เป็นไปได้ด้วยต้นทุนประสิทธิภาพเล็กน้อยและไม่มีข้อเสียอื่น ๆ ?
การไม่สามารถทำนายพฤติกรรมโดยไม่ลองใช้ตัวอย่างโค้ดหมายความว่าไม่เข้าใจแนวคิด ด้านล่างนี้คือสิ่งที่ช่วยให้ฉันเข้าใจการสืบทอดเสมือน
ดับเบิ้ลเอ
ก่อนอื่นให้เริ่มต้นด้วยรหัสนี้โดยไม่มีการสืบทอดเสมือน:
#include<iostream>
using namespace std;
class A {
public:
A() { cout << "A::A() "; }
A(int x) : m_x(x) { cout << "A::A(" << x << ") "; }
int getX() const { return m_x; }
private:
int m_x = 42;
};
class B : public A {
public:
B(int x):A(x) { cout << "B::B(" << x << ") "; }
};
class C : public A {
public:
C(int x):A(x) { cout << "C::C(" << x << ") "; }
};
class D : public C, public B {
public:
D(int x, int y): C(x), B(y) {
cout << "D::D(" << x << ", " << y << ") "; }
};
int main() {
cout << "Create b(2): " << endl;
B b(2); cout << endl << endl;
cout << "Create c(3): " << endl;
C c(3); cout << endl << endl;
cout << "Create d(2,3): " << endl;
D d(2, 3); cout << endl << endl;
cout << "d.B::getX() = " << d.B::getX() << endl;
cout << "d.C::getX() = " << d.C::getX() << endl;
}
ให้ผ่านเอาต์พุต การดำเนินการB b(2);
สร้างA(2)
ตามที่คาดไว้เช่นเดียวกับC c(3);
:
Create b(2):
A::A(2) B::B(2)
Create c(3):
A::A(3) C::C(3)
D d(2, 3);
ต้องการทั้งสองอย่างB
และC
แต่ละคนสร้างของตัวเองA
ดังนั้นเราจึงมีสองเท่าA
ในd
:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
นั่นเป็นสาเหตุd.getX()
ที่ทำให้เกิดข้อผิดพลาดในการคอมไพเลอร์เนื่องจากคอมไพเลอร์ไม่สามารถเลือกA
อินสแตนซ์ที่ควรเรียกใช้เมธอดได้ ยังคงเป็นไปได้ที่จะเรียกเมธอดโดยตรงสำหรับคลาสแม่ที่เลือก:
d.B::getX() = 3
d.C::getX() = 2
ความเสมือนจริง
ตอนนี้ให้เพิ่มการสืบทอดเสมือน ใช้ตัวอย่างโค้ดเดียวกันกับการเปลี่ยนแปลงต่อไปนี้:
class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl;
cout << "d.A::getX() = " << d.A::getX() << endl;
...
ให้ข้ามไปที่การสร้างd
:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
คุณสามารถมองเห็นA
ถูกสร้างขึ้นด้วยคอนสตรัคไม่สนใจค่าเริ่มต้นที่ส่งผ่านจากการก่อสร้างของและB
C
เมื่อความคลุมเครือหายไปการเรียกทั้งหมดให้getX()
ส่งคืนค่าเดียวกัน:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
แต่ถ้าเราต้องการเรียกตัวสร้าง parametrized เพื่อA
อะไร? สามารถทำได้โดยเรียกอย่างชัดเจนจากตัวสร้างของD
:
D(int x, int y, int z): A(x), C(y), B(z)
โดยปกติคลาสสามารถใช้คอนสตรัคเตอร์ของผู้ปกครองโดยตรงเท่านั้น แต่มีข้อยกเว้นสำหรับกรณีการสืบทอดเสมือน การค้นพบกฎนี้ "คลิก" สำหรับฉันและช่วยให้เข้าใจอินเทอร์เฟซเสมือนจริงได้มาก:
รหัสclass B: virtual A
หมายความว่าคลาสใด ๆ ที่สืบทอดมาB
จะต้องรับผิดชอบในการสร้างA
ด้วยตัวเองเนื่องจากB
จะไม่ทำโดยอัตโนมัติ
เมื่อคำนึงถึงข้อความนี้คุณจึงสามารถตอบคำถามทั้งหมดที่มีได้อย่างง่ายดาย:
- ในระหว่าง
D
การสร้างค่าB
มิได้C
เป็นผู้รับผิดชอบค่าพารามิเตอร์ของA
มันทั้งหมดขึ้นอยู่กับD
เพียง
C
จะมอบหมายการสร้างA
ให้D
แต่B
จะสร้างอินสแตนซ์ของตัวเองA
เพื่อนำปัญหาเพชรกลับมา
- การกำหนดพารามิเตอร์ระดับพื้นฐานในคลาสหลานแทนที่จะเป็นลูกโดยตรงไม่ใช่แนวทางปฏิบัติที่ดีดังนั้นจึงควรยอมรับเมื่อมีปัญหาเพชรอยู่และมาตรการนี้ไม่สามารถหลีกเลี่ยงได้