Tại sao câu trả lời khác?
Chà, nhiều bài viết trên SO và các bài báo bên ngoài nói rằng, vấn đề kim cương được giải quyết bằng cách tạo một phiên bản duy nhất A
thay vì hai (một cho mỗi cha của D
), do đó giải quyết được sự mơ hồ. Tuy nhiên, điều này không cung cấp cho tôi hiểu biết toàn diện về quy trình, tôi đã kết thúc với nhiều câu hỏi hơn như
- điều gì xảy ra nếu
B
và C
cố gắng tạo ra các trường hợp khác nhau, ví A
dụ như gọi hàm tạo tham số với các tham số khác nhau ( D::D(int x, int y): C(x), B(y) {}
)? Trường hợp nào A
sẽ được chọn để trở thành một phần của D
?
- điều gì sẽ xảy ra nếu tôi sử dụng kế thừa không phải ảo
B
, mà là kế thừa ảo để làm C
gì? Nó có đủ để tạo một phiên bản duy nhất của A
in D
không?
- Tôi có nên luôn sử dụng kế thừa ảo theo mặc định từ bây giờ như một biện pháp phòng ngừa vì nó giải quyết vấn đề kim cương có thể xảy ra với chi phí hiệu suất nhỏ và không có nhược điểm nào khác không?
Không thể dự đoán hành vi mà không thử các mẫu mã có nghĩa là không hiểu khái niệm. Dưới đây là những gì đã giúp tôi xoay quanh vấn đề thừa kế ảo.
Gấp đôi
Đầu tiên, hãy bắt đầu với mã này mà không có kế thừa ảo:
#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;
// error: request for member 'getX' is ambiguous
//cout << "d.getX() = " << d.getX() << endl;
// error: 'A' is an ambiguous base of 'D'
//cout << "d.A::getX() = " << d.A::getX() << endl;
cout << "d.B::getX() = " << d.B::getX() << endl;
cout << "d.C::getX() = " << d.C::getX() << endl;
}
Cho phép đi qua đầu ra. Việc thực thi B b(2);
sẽ tạo ra A(2)
như mong đợi, tương tự cho 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);
cần cả hai B
và C
, mỗi người trong số họ tạo ra của riêng mình A
, vì vậy chúng tôi có gấp đôi A
trong d
:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
Đó là lý do d.getX()
gây ra lỗi biên dịch vì trình biên dịch không thể chọn thể hiện mà A
nó sẽ gọi phương thức. Vẫn có thể gọi các phương thức trực tiếp cho lớp cha đã chọn:
d.B::getX() = 3
d.C::getX() = 2
Đức hạnh
Bây giờ hãy thêm thừa kế ảo. Sử dụng cùng một mẫu mã với các thay đổi sau:
class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...
Hãy chuyển sang tạo d
:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Bạn có thể thấy, A
được tạo với hàm tạo mặc định bỏ qua các tham số được truyền từ các hàm tạo của B
và C
. Khi sự mơ hồ biến mất, tất cả các lệnh gọi getX()
trả về cùng một giá trị:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Nhưng nếu chúng ta muốn gọi hàm tạo tham số để làm A
gì? Nó có thể được thực hiện bằng cách gọi nó một cách rõ ràng từ hàm tạo của D
:
D(int x, int y, int z): A(x), C(y), B(z)
Thông thường, lớp có thể chỉ sử dụng các hàm tạo của cha mẹ trực tiếp một cách rõ ràng, nhưng có một trường hợp loại trừ đối với trường hợp thừa kế ảo. Việc khám phá quy tắc này đã "kích" tôi và giúp tôi hiểu rất nhiều về giao diện ảo:
Mã class B: virtual A
có nghĩa là bất kỳ lớp nào được kế thừa từ B
bây giờ chịu trách nhiệm tạo A
bởi chính nó, vì B
nó sẽ không tự động thực hiện.
Với câu nói này, thật dễ dàng để trả lời tất cả các câu hỏi tôi có:
- Trong quá trình
D
tạo B
cũng không C
chịu trách nhiệm về các tham số của A
, nó hoàn toàn D
chỉ.
C
sẽ ủy tạo A
để D
, nhưng B
sẽ tạo ra ví dụ riêng của mình A
do đó mang vấn đề kim cương trở lại
- Việc xác định các tham số lớp cơ sở trong lớp cháu thay vì lớp con trực tiếp không phải là một phương pháp hay, vì vậy nó nên được chấp nhận khi có vấn đề kim cương và biện pháp này là không thể tránh khỏi.