Các chức năng ảo và vtable được thực hiện như thế nào?


109

Tất cả chúng ta đều biết các hàm ảo là gì trong C ++, nhưng chúng được triển khai ở cấp độ sâu như thế nào?

Vtable có thể được sửa đổi hoặc thậm chí được truy cập trực tiếp trong thời gian chạy không?

Vtable tồn tại cho tất cả các lớp hay chỉ những lớp có ít nhất một hàm ảo?

Các lớp trừu tượng có đơn giản chỉ có NULL cho con trỏ hàm của ít nhất một mục nhập không?

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.


2
Đề nghị đọc kiệt tác Inside the C++ Object Modelcủa Stanley B. Lippman. (Phần 4.2, trang 124-131)
smwikipedia

Câu trả lời:


123

Các chức năng ảo được thực hiện ở cấp độ sâu như thế nào?

Từ "Hàm ảo trong C ++" :

Bất cứ khi nào một chương trình có một hàm ảo được khai báo, bảng av được xây dựng cho lớp. Bảng v bao gồm các địa chỉ đến các hàm ảo cho các lớp có chứa một hoặc nhiều hàm ảo. Đối tượng của lớp chứa hàm ảo chứa một con trỏ ảo trỏ đến địa chỉ cơ sở của bảng ảo trong bộ nhớ. Bất cứ khi nào có lệnh gọi hàm ảo, bảng v được sử dụng để phân giải địa chỉ hàm. Một đối tượng của lớp có chứa một hoặc nhiều hàm ảo chứa một con trỏ ảo được gọi là vptr ở ngay đầu đối tượng trong bộ nhớ. Do đó kích thước của đối tượng trong trường hợp này tăng lên bằng kích thước của con trỏ. Vptr này chứa địa chỉ cơ sở của bảng ảo trong bộ nhớ. Lưu ý rằng các bảng ảo là lớp cụ thể, tức là, chỉ có một bảng ảo cho một lớp bất kể số lượng hàm ảo mà nó chứa. Bảng ảo này lần lượt chứa các địa chỉ cơ sở của một hoặc nhiều hàm ảo của lớp. Tại thời điểm khi một hàm ảo được gọi trên một đối tượng, vptr của đối tượng đó cung cấp địa chỉ cơ sở của bảng ảo cho lớp đó trong bộ nhớ. Bảng này được sử dụng để giải quyết cuộc gọi hàm vì nó chứa địa chỉ của tất cả các hàm ảo của lớp đó. Đây là cách liên kết động được giải quyết trong khi gọi hàm ảo. vptr của đối tượng đó cung cấp địa chỉ cơ sở của bảng ảo cho lớp đó trong bộ nhớ. Bảng này được sử dụng để giải quyết cuộc gọi hàm vì nó chứa địa chỉ của tất cả các hàm ảo của lớp đó. Đây là cách liên kết động được giải quyết trong khi gọi hàm ảo. vptr của đối tượng đó cung cấp địa chỉ cơ sở của bảng ảo cho lớp đó trong bộ nhớ. Bảng này được sử dụng để giải quyết cuộc gọi hàm vì nó chứa địa chỉ của tất cả các hàm ảo của lớp đó. Đây là cách liên kết động được giải quyết trong khi gọi hàm ảo.

Vtable có thể được sửa đổi hoặc thậm chí được truy cập trực tiếp trong thời gian chạy không?

Về mặt đại học, tôi tin rằng câu trả lời là "không". Bạn có thể thực hiện một số thao tác xáo trộn bộ nhớ để tìm vtable nhưng bạn vẫn không biết chữ ký hàm trông như thế nào để gọi nó. Bất cứ điều gì bạn muốn đạt được với khả năng này (ngôn ngữ hỗ trợ) đều có thể thực hiện được mà không cần truy cập trực tiếp vào vtable hoặc sửa đổi nó trong thời gian chạy. Cũng lưu ý, đặc tả ngôn ngữ C ++ không chỉ định rằng các vtables là bắt buộc - tuy nhiên đó là cách hầu hết các trình biên dịch thực hiện các hàm ảo.

Vtable tồn tại cho tất cả các đối tượng hay chỉ những đối tượng có ít nhất một chức năng ảo?

Tôi tin rằng câu trả lời ở đây là "nó phụ thuộc vào việc triển khai" vì spec không yêu cầu vtables ngay từ đầu. Tuy nhiên, trong thực tế, tôi tin rằng tất cả các trình biên dịch hiện đại chỉ tạo một vtable nếu một lớp có ít nhất 1 hàm ảo. Có chi phí không gian liên quan đến vtable và chi phí thời gian được liên kết với việc gọi một hàm ảo so với một hàm không ảo.

Các lớp trừu tượng có đơn giản chỉ có NULL cho con trỏ hàm của ít nhất một mục nhập không?

Câu trả lời là nó không được xác định bởi đặc tả ngôn ngữ vì vậy nó phụ thuộc vào việc triển khai. Việc gọi hàm thuần ảo dẫn đến hành vi không xác định nếu nó không được xác định (mà nó thường không được xác định) (ISO / IEC 14882: 2003 10.4-2). Trong thực tế, nó cấp phát một vị trí trong vtable cho hàm nhưng không gán địa chỉ cho nó. Điều này làm cho vtable không đầy đủ yêu cầu các lớp dẫn xuất để thực hiện chức năng và hoàn thành vtable. Một số triển khai chỉ đơn giản là đặt một con trỏ NULL vào mục nhập vtable; các triển khai khác đặt một con trỏ đến một phương thức giả thực hiện điều gì đó tương tự như một khẳng định.

Lưu ý rằng một lớp trừu tượng có thể xác định một triển khai cho một hàm ảo thuần túy, nhưng hàm đó chỉ có thể được gọi với cú pháp đủ điều kiện-id (tức là, chỉ định đầy đủ lớp trong tên phương thức, tương tự như gọi một phương thức lớp cơ sở từ một Lớp có nguồn gốc). Điều này được thực hiện để cung cấp một cài đặt mặc định dễ sử dụng, trong khi vẫn yêu cầu lớp dẫn xuất cung cấp ghi đè.

Việc có một hàm ảo duy nhất có làm chậm cả lớp hay chỉ lệnh gọi hàm ảo?

Điều này đang nằm trong tầm hiểu biết của tôi, vì vậy ai đó hãy giúp tôi nếu tôi sai!

Tôi tin rằng chỉ các hàm ảo trong lớp trải nghiệm hiệu suất thời gian liên quan đến việc gọi một hàm ảo so với một hàm không ảo. Không gian trên cao cho lớp học là có một trong hai cách. Lưu ý rằng nếu có vtable, chỉ có 1 cho mỗi lớp , không phải một cho mỗi đối tượng .

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?

Tôi không tin rằng thời gian thực thi của một hàm ảo bị ghi đè sẽ giảm so với việc gọi hàm ảo cơ sở. Tuy nhiên, có một khoảng trống bổ sung cho lớp được liên kết với việc xác định một vtable khác cho lớp dẫn xuất so với lớp cơ sở.

Tài nguyên bổ sung:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (qua máy quay lại)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
Nó sẽ không phù hợp với triết lý của Stroustrup về C ++ đối với một trình biên dịch để đặt một con trỏ vtable không cần thiết vào một đối tượng không cần nó. Quy tắc là bạn không nhận được chi phí không có trong C trừ khi bạn yêu cầu, và các trình biên dịch phá vỡ điều đó là vô lễ.
Steve Jessop 19-08

3
Tôi đồng ý rằng sẽ là ngu ngốc đối với bất kỳ trình biên dịch nào coi trọng bản thân nó khi sử dụng vtable khi không có chức năng ảo nào tồn tại. Tuy nhiên, tôi cảm thấy điều quan trọng là phải chỉ ra rằng, theo hiểu biết của tôi, tiêu chuẩn C ++ không / yêu cầu / nó, vì vậy hãy cảnh báo trước khi tùy thuộc vào nó.
Zach Burlingame

8
Ngay cả các hàm ảo cũng có thể được gọi là không ảo. Điều này trên thực tế khá phổ biến: nếu đối tượng nằm trên ngăn xếp, trong phạm vi, trình biên dịch sẽ biết chính xác loại và tối ưu hóa việc tra cứu vtable. Điều này đặc biệt đúng với dtor, nó phải được gọi trong cùng một phạm vi ngăn xếp.
MSalters

1
Tôi tin rằng khi một lớp có ít nhất một hàm ảo, mọi đối tượng đều có một vtable, và không phải một cho toàn bộ lớp.
Asaf R

3
Thực hiện chung: Mỗi đối tượng có một con trỏ đến một vtable; lớp sở hữu bảng. Phép thuật xây dựng chỉ đơn giản là cập nhật con trỏ vtable trong ctor dẫn xuất, sau khi ctor cơ sở kết thúc.
MSalters

31
  • Vtable có thể được sửa đổi hoặc thậm chí được truy cập trực tiếp trong thời gian chạy không?

Không đáng kể, nhưng nếu bạn không ngại những trò bẩn thỉu, chắc chắn!

CẢNH BÁO : Kỹ thuật này không được khuyến khích sử dụng cho trẻ em, người lớn dưới 969 tuổi hoặc các sinh vật nhỏ có lông từ Alpha Centauri. Các tác dụng phụ có thể bao gồm ma quỷ bay ra khỏi mũi của bạn , sự xuất hiện đột ngột của Yog-Sothoth với tư cách là người phê duyệt bắt buộc trên tất cả các lần đánh giá mã tiếp theo hoặc bổ sung hồi tố IHuman::PlayPiano()cho tất cả các phiên bản hiện có]

Trong hầu hết các trình biên dịch mà tôi đã thấy, vtbl * là 4 byte đầu tiên của đối tượng và nội dung vtbl chỉ đơn giản là một mảng các con trỏ thành viên ở đó (thường theo thứ tự chúng được khai báo, với lớp cơ sở đầu tiên). Tất nhiên có những bố cục khả thi khác, nhưng đó là những gì tôi thường quan sát thấy.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Bây giờ để kéo một số tai quái ...

Thay đổi lớp trong thời gian chạy:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Thay thế một phương thức cho tất cả các trường hợp (bắt cặp một lớp)

Điều này phức tạp hơn một chút, vì bản thân vtbl có thể nằm trong bộ nhớ chỉ đọc.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

Cái thứ hai có nhiều khả năng làm cho các trình kiểm tra vi-rút và liên kết thức dậy và chú ý, do các thao tác bảo vệ. Trong quá trình sử dụng bit NX, nó có thể bị lỗi.


6
Hừ! Thật đáng ngại khi điều này nhận được một khoản tiền thưởng. Tôi hy vọng điều đó không có nghĩa là @Mobilewits cho rằng những trò tai quái như vậy thực sự là một ý kiến ​​hay ...
puetzk

1
Vui lòng xem xét việc không khuyến khích sử dụng kỹ thuật này, rõ ràng và mạnh mẽ, thay vì "nháy mắt".
einpoklum

" Nội dung vtbl chỉ đơn giản là một mảng của con trỏ thành viên " thực sự đó là một kỷ lục (một struct) với mục khác nhau, điều đó xảy ra được cách nhau đều đặn
curiousguy

1
Bạn có thể nhìn vào nó theo một trong hai cách; các con trỏ hàm có các chữ ký khác nhau và do đó các kiểu con trỏ khác nhau; theo nghĩa đó, nó thực sự giống như cấu trúc. Nhưng trong các ngữ cảnh khác, nhưng ý tưởng về chỉ mục vtbl là hữu ích (ví dụ: ActiveX sử dụng nó theo cách nó mô tả giao diện kép trong typelibs), là một dạng xem giống mảng hơn.
puetzk

17

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 charthà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 argthuộ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ì vtableluôn là phần tử đầu tiên tại địa chỉ của đối tượng, bất kể nó được gọi vtablehay base.vtabletrong một biểu thức được nhập đúng.


"Mọi đối tượng của một lớp đa hình sẽ trỏ đến vtable của riêng nó." Bạn đang nói mọi đối tượng đều có vtable của riêng nó? AFAIK vtable được chia sẻ giữa tất cả các đối tượng của cùng một lớp. Hãy cho tôi biết nếu tôi sai.
Bhuwan

1
@Bhuwan: Không, bạn nói đúng: chỉ có một vtable cho mỗi loại (có thể là cho mỗi mẫu khởi tạo trong trường hợp có mẫu). Tôi muốn nói rằng mỗi đối tượng của một lớp đa hình có trỏ đến vtable áp dụng cho nó, vì vậy mỗi đối tượng có một con trỏ như vậy, nhưng đối với các đối tượng cùng loại, nó sẽ trỏ đến cùng một bảng. Có lẽ tôi nên viết lại điều này.
MvG

1
@MvG " các đối tượng cùng kiểu nó sẽ trỏ đến cùng một bảng " không phải trong quá trình xây dựng các lớp cơ sở với các lớp cơ sở ảo! (một trường hợp rất đặc biệt)
curiousguy

1
@curiousguy: Tôi muốn khai báo rằng theo "phần trên được đơn giản hóa theo nhiều cách", đặc biệt vì ứng dụng chính của cơ sở ảo là đa kế thừa, mà tôi cũng không mô hình hóa. Nhưng cảm ơn bạn đã nhận xét, rất hữu ích khi có điều này ở đây cho những người có thể cần thêm thông tin chuyên sâu.
MvG

3

Thông thường với VTable, một mảng con trỏ tới các hàm.


2

Câu trả lời này đã được kết hợp vào câu trả lời trên Wiki Cộng đồng

  • Các lớp trừu tượng có đơn giản chỉ có NULL cho con trỏ hàm của ít nhất một mục nhập không?

Câu trả lời cho điều đó là nó không xác định - việc gọi hàm thuần ảo dẫn đến hành vi không xác định nếu nó không được xác định (mà nó thường không được xác định) (ISO / IEC 14882: 2003 10.4-2). Một số triển khai chỉ đơn giản là đặt một con trỏ NULL vào mục nhập vtable; các triển khai khác đặt một con trỏ đến một phương thức giả thực hiện điều gì đó tương tự như một khẳng định.

Lưu ý rằng một lớp trừu tượng có thể xác định một triển khai cho một hàm ảo thuần túy, nhưng hàm đó chỉ có thể được gọi với cú pháp đủ điều kiện-id (tức là, chỉ định đầy đủ lớp trong tên phương thức, tương tự như gọi một phương thức lớp cơ sở từ một Lớp có nguồn gốc). Điều này được thực hiện để cung cấp một cài đặt mặc định dễ sử dụng, trong khi vẫn yêu cầu lớp dẫn xuất cung cấp ghi đè.


Ngoài ra, tôi không nghĩ rằng một lớp trừu tượng có thể định nghĩa việc triển khai cho một hàm ảo thuần túy. Theo định nghĩa, một hàm ảo thuần túy không có phần thân (ví dụ: bool my_func () = 0;). Tuy nhiên, bạn có thể cung cấp các triển khai cho các hàm ảo thông thường.
Zach Burlingame 19-08

Một hàm ảo thuần túy có thể có một định nghĩa. Xem Mục # 34 "Hiệu quả C ++, Phiên bản thứ 3" của Scott Meyers, ISO 14882-2003 10.4-2 hoặc bytes.com/forum/thread572745.html
Michael Burr

2

Bạn có thể tạo lại chức năng của các hàm ảo trong C ++ bằng cách sử dụng con trỏ hàm làm thành viên của một lớp và các hàm tĩnh như là các triển khai hoặc sử dụng con trỏ tới các hàm thành viên và các hàm thành viên để triển khai. Chỉ có lợi thế về mặt ký hiệu giữa hai phương pháp ... trong thực tế, các cuộc gọi hàm ảo chỉ là một tiện ích ký hiệu. Trên thực tế, kế thừa chỉ là một sự tiện lợi về mặt ký hiệu ... tất cả đều có thể được thực hiện mà không cần sử dụng các tính năng ngôn ngữ để kế thừa. :)

Dưới đây là đoạn mã chưa được kiểm tra, có thể là lỗi, nhưng hy vọng sẽ chứng minh được ý tưởng.

ví dụ

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

void(*)(Foo*) MyFunc;đây có phải là một số cú pháp Java?
curiousguy

không, cú pháp C / C ++ của nó cho con trỏ hàm. Để trích dẫn bản thân tôi "Bạn có thể tạo lại chức năng của các hàm ảo trong C ++ bằng cách sử dụng con trỏ hàm". cú pháp của nó là một chút khó chịu, nhưng là điều cần làm quen nếu bạn coi mình là một lập trình viên C.
jheriko

Con trỏ hàm ac sẽ trông giống như sau: int ( PROC) (); và một con trỏ đến một hàm thành viên lớp sẽ giống như sau: int (ClassName :: MPROC) ();
Menace

1
@menace, bạn quên một số cú pháp ở đó ... bạn đang nghĩ đến typedef? typedef int (* PROC) (); vì vậy bạn chỉ có thể thực hiện PROC foo sau thay vì int (* foo) ()?
jheriko

2

Tôi sẽ cố gắng làm cho nó đơn giản :)

Tất cả chúng ta đều biết các hàm ảo là gì trong C ++, nhưng chúng được triển khai ở cấp độ sâu như thế nào?

Đây là một mảng với các con trỏ đến các hàm, là các triển khai của một hàm ảo cụ thể. Một chỉ mục trong mảng này đại diện cho chỉ số cụ thể của một hàm ảo được xác định cho một lớp. Điều này bao gồm các chức năng ảo thuần túy.

Khi một lớp đa hình dẫn xuất từ ​​một lớp đa hình khác, chúng ta có thể gặp các trường hợp sau:

  • Lớp dẫn xuất không thêm các hàm ảo mới cũng như không ghi đè bất kỳ. Trong trường hợp này, lớp này chia sẻ vtable với lớp cơ sở.
  • Lớp dẫn xuất thêm và ghi đè các phương thức ảo. Trong trường hợp này, nó nhận được vtable của riêng nó, trong đó các hàm ảo được thêm vào có chỉ mục bắt đầu sau hàm dẫn xuất cuối cùng.
  • Nhiều lớp đa hình trong thừa kế. Trong trường hợp này, chúng ta có sự thay đổi chỉ mục giữa cơ sở thứ hai và cơ sở tiếp theo và chỉ mục của nó trong lớp dẫn xuất

Vtable có thể được sửa đổi hoặc thậm chí được truy cập trực tiếp trong thời gian chạy không?

Không phải là cách chuẩn - không có API nào để truy cập chúng. Các trình biên dịch có thể có một số tiện ích mở rộng hoặc API riêng để truy cập chúng, nhưng đó có thể chỉ là một tiện ích mở rộng.

Vtable tồn tại cho tất cả các lớp hay chỉ những lớp có ít nhất một hàm ảo?

Chỉ những hàm có ít nhất một hàm ảo (thậm chí là hàm hủy) hoặc dẫn xuất ít nhất một lớp có vtable của nó ("là đa hình").

Các lớp trừu tượng có đơn giản chỉ có NULL cho con trỏ hàm của ít nhất một mục nhập không?

Đó là một cách thực hiện có thể, nhưng đúng hơn là không được thực hành. Thay vào đó, thường có một hàm in ra một cái gì đó như "hàm ảo thuần túy được gọi là" và hiện abort(). Lệnh gọi đến đó có thể xảy ra nếu bạn cố gắng gọi phương thức trừu tượng trong hàm tạo hoặc hàm hủy.

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 làm chậm chỉ phụ thuộc vào việc cuộc gọi được giải quyết dưới dạng cuộc gọi trực tiếp hay cuộc gọi ảo. Không còn vấn đề gì nữa. :)

Nếu bạn gọi một hàm ảo thông qua một con trỏ hoặc tham chiếu đến một đối tượng, thì nó sẽ luôn được triển khai dưới dạng lệnh gọi ảo - bởi vì trình biên dịch không bao giờ có thể biết loại đối tượng nào sẽ được gán cho con trỏ này trong thời gian chạy và liệu nó có phải là một lớp mà phương thức này có bị ghi đè hay không. Chỉ trong hai trường hợp, trình biên dịch có thể giải quyết cuộc gọi đến một hàm ảo dưới dạng một cuộc gọi trực tiếp:

  • Nếu bạn gọi phương thức thông qua một giá trị (một biến hoặc kết quả của một hàm trả về giá trị) - trong trường hợp này, trình biên dịch không nghi ngờ lớp thực sự của đối tượng là gì và có thể "giải quyết" nó tại thời điểm biên dịch .
  • Nếu phương thức ảo được khai báo finaltrong lớp mà bạn có con trỏ hoặc tham chiếu mà bạn gọi nó ( chỉ trong C ++ 11 ). Trong trường hợp này, trình biên dịch biết rằng phương thức này không thể bị ghi đè thêm nữa và nó chỉ có thể là phương thức từ lớp này.

Xin lưu ý rằng các cuộc gọi ảo chỉ có chi phí tham chiếu hai con trỏ. Sử dụng RTTI (mặc dù chỉ có sẵn cho các lớp đa hình) chậm hơn so với việc gọi các phương thức ảo, nếu bạn tìm thấy một trường hợp để triển khai cùng một điều theo hai cách như vậy. Ví dụ: xác định virtual bool HasHoof() { return false; }và sau đó chỉ ghi đè bool Horse::HasHoof() { return true; }sẽ cung cấp cho bạn khả năng gọi if (anim->HasHoof())sẽ nhanh hơn việc thử if(dynamic_cast<Horse*>(anim)). Điều này là do dynamic_castphải xem qua hệ thống phân cấp lớp trong một số trường hợp thậm chí là đệ quy để xem liệu có thể xây dựng đường dẫn từ kiểu con trỏ thực tế và kiểu lớp mong muốn hay không. Trong khi cuộc gọi ảo luôn giống nhau - tham chiếu đến hai con trỏ.


2

Đây là cách triển khai thủ công có thể chạy được của bảng ảo trong C ++ hiện đại. Nó có ngữ nghĩa được xác định rõ ràng, không có hack và không void*.

Lưu ý: .*->*là các toán tử khác với *->. Con trỏ hàm thành viên hoạt động khác nhau.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

1

Mỗi đối tượng có một con trỏ vtable trỏ đến một mảng các hàm thành viên.


1

Điều gì đó không được đề cập ở đây trong tất cả các câu trả lời này là trong trường hợp đa kế thừa, trong đó các lớp cơ sở đều có các phương thức ảo. Lớp kế thừa có nhiều con trỏ đến một vmt. Kết quả là kích thước của mỗi thể hiện của một đối tượng như vậy lớn hơn. Mọi người đều biết rằng một lớp có phương thức ảo có thêm 4 byte cho vmt, nhưng trong trường hợp đa kế thừa, mỗi lớp cơ sở có các phương thức ảo gấp 4 lần kích thước của con trỏ.


0

Câu trả lời của Burly là đúng ở đây ngoại trừ câu hỏi:

Các lớp trừu tượng có đơn giản chỉ có NULL cho con trỏ hàm của ít nhất một mục nhập không?

Câu trả lời là không có bảng ảo nào được tạo cho các lớp trừu tượng. Không cần thiết vì không có đối tượng nào của các lớp này có thể được tạo!

Nói cách khác nếu chúng ta có:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

Con trỏ vtbl được truy cập thông qua pB sẽ là vtbl của lớp D. Đây chính xác là cách thực hiện đa hình. Đó là, cách các phương thức D được truy cập thông qua pB. Không cần vtbl cho lớp B.

Đáp lại bình luận của Mike bên dưới ...

Nếu lớp B trong mô tả của tôi có phương thức ảo foo () không bị D ghi đè và thanh phương thức ảo () bị ghi đè, thì vtbl của D sẽ có một con trỏ đến foo của B () và đến thanh của chính nó () . Vẫn chưa có vtbl nào được tạo cho B.


Điều này không đúng vì 2 lý do: 1) một lớp trừu tượng có thể có các phương thức ảo thông thường bên cạnh các phương thức ảo thuần túy và 2) các phương thức thuần ảo có thể tùy chọn có một định nghĩa có thể được gọi với một tên đủ điều kiện.
Michael Burr 19-08

Đúng - suy nghĩ thứ hai, tôi tưởng tượng rằng nếu tất cả các phương thức ảo là ảo thuần túy thì trình biên dịch có thể tối ưu hóa vtable đi (nó sẽ cần trợ giúp hình thành trình liên kết để đảm bảo không có định nghĩa nào).
Michael Burr 19-08

1
Câu trả lời là không có bảng ảo nào được tạo cho các lớp trừu tượng. ” Sai. " Không cần thiết vì không có đối tượng nào của các lớp này có thể được tạo! " Sai.
curiousguy

Tôi có thể làm theo lý do của bạn mà không cho vtable B nên cần thiết. Chỉ vì một số phương thức của nó có triển khai (mặc định) không có nghĩa là chúng phải được lưu trữ trong vtable. Nhưng tôi chỉ chạy mã của bạn (modulo một số bản sửa lỗi để làm cho nó biên dịch) gcc -Stheo sau c++filtvà rõ ràng là có một vtable để Bđưa vào đó. Tôi đoán đó có thể là do vtable cũng lưu trữ dữ liệu RTTI như tên lớp và kế thừa. Nó có thể được yêu cầu cho một dynamic_cast<B*>. Thậm chí -fno-rttikhông làm cho vtable mất đi. Với clang -O3thay vì gccnó đột ngột biến mất.
MvG

@MvG " Chỉ vì một số phương thức của nó có triển khai (mặc định) không có nghĩa là chúng phải được lưu trữ trong vtable " Có, nó có nghĩa là chỉ vậy.
curiousguy

0

bằng chứng rất dễ thương về khái niệm mà tôi đã đưa ra trước đó một chút (để xem thứ tự kế thừa có quan trọng không); cho tôi biết nếu việc triển khai C ++ của bạn thực sự từ chối nó (phiên bản gcc của tôi chỉ đưa ra cảnh báo về việc gán cấu trúc ẩn danh, nhưng đó là một lỗi), tôi tò mò.

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

đầu ra:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

lưu ý vì tôi không bao giờ phân bổ đối tượng giả của mình, nên không cần thực hiện bất kỳ việc tiêu hủy nào; các hàm hủy được tự động đặt ở cuối phạm vi của các đối tượng được cấp phát động để lấy lại bộ nhớ của chính đối tượng và con trỏ vtable.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.