Việc có một chức năng ảo duy nhất có làm chậm cả lớp không?
Hay chỉ có lời gọi hàm là ảo? Và tốc độ có bị ảnh hưởng nếu chức năng ảo thực sự bị ghi đè hay không, hoặc điều này không ảnh hưởng gì nếu nó là ảo.
Việc có các hàm ảo làm chậm toàn bộ lớp trong chừng mực vì phải khởi tạo, sao chép thêm một mục dữ liệu,… khi xử lý một đối tượng của một lớp như vậy. Đối với một lớp có khoảng nửa tá thành viên, sự khác biệt hẳn là không đáng kể. Đối với một lớp chỉ chứa một char
thành viên duy nhất hoặc không có thành viên nào, sự khác biệt có thể đáng chú ý.
Ngoài ra, điều quan trọng cần lưu ý là không phải mọi lệnh gọi hàm ảo đều là lệnh gọi hàm ảo. Nếu bạn có một đối tượng thuộc loại đã biết, trình biên dịch có thể phát ra mã cho một lệnh gọi hàm bình thường và thậm chí có thể nội dòng hàm đã nói nếu bạn cảm thấy thích. Chỉ khi bạn thực hiện các lệnh gọi đa hình, thông qua một con trỏ hoặc tham chiếu có thể trỏ đến một đối tượng của lớp cơ sở hoặc vào một đối tượng của một số lớp dẫn xuất, bạn mới cần hướng dẫn vtable và trả tiền cho nó về mặt hiệu suất.
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
Các bước phần cứng phải thực hiện về cơ bản là giống nhau, bất kể chức năng có bị ghi đè hay không. Địa chỉ của vtable được đọc từ đối tượng, con trỏ hàm được truy xuất từ vị trí thích hợp và hàm được gọi bởi con trỏ. Về hiệu suất thực tế, các dự đoán về nhánh có thể có một số tác động. Vì vậy, ví dụ: nếu hầu hết các đối tượng của bạn tham chiếu đến cùng một triển khai của một hàm ảo nhất định, thì có một số cơ hội mà bộ dự đoán nhánh sẽ dự đoán chính xác hàm nào sẽ gọi ngay cả trước khi con trỏ được truy xuất. Nhưng không quan trọng chức năng nào là chức năng phổ biến: nó có thể là hầu hết các đối tượng ủy quyền cho trường hợp cơ sở không bị ghi đè, hoặc hầu hết các đối tượng thuộc cùng một lớp con và do đó ủy quyền cho cùng một trường hợp được ghi đè.
chúng được thực hiện như thế nào ở mức độ sâu?
Tôi thích ý tưởng về jheriko để chứng minh điều này bằng cách sử dụng một triển khai mô phỏng. Nhưng tôi sẽ sử dụng C để triển khai một cái gì đó tương tự như đoạn mã ở trên, để cấp thấp dễ dàng nhìn thấy hơn.
lớp cha mẹ Foo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
lớp dẫn xuất Bar
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
hàm f thực hiện lệnh gọi hàm ảo
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
Vì vậy, bạn có thể thấy, một vtable chỉ là một khối tĩnh trong bộ nhớ, chủ yếu chứa các con trỏ hàm. Mọi đối tượng của một lớp đa hình sẽ trỏ đến vtable tương ứng với kiểu động của nó. Điều này cũng làm cho mối liên hệ giữa RTTI và các hàm ảo rõ ràng hơn: bạn có thể kiểm tra loại lớp đơn giản bằng cách xem vtable nó trỏ đến. Phần trên được đơn giản hóa theo nhiều cách, chẳng hạn như đa kế thừa, nhưng khái niệm chung là hợp lý.
Nếu arg
thuộc loại Foo*
và bạn lấy arg->vtable
, nhưng thực sự là một đối tượng của loại Bar
, thì bạn vẫn nhận được địa chỉ chính xác của vtable
. Đó là bởi vì vtable
luôn là phần tử đầu tiên tại địa chỉ của đối tượng, bất kể nó được gọi vtable
hay base.vtable
trong một biểu thức được nhập đúng.
Inside the C++ Object Model
củaStanley B. Lippman
. (Phần 4.2, trang 124-131)