C ++ từ khóa ảo ảo cho các chức năng trong các lớp dẫn xuất. Có cần thiết không?


221

Với định nghĩa cấu trúc được đưa ra dưới đây ...

struct A {
    virtual void hello() = 0;
};

Cách tiếp cận số 1:

struct B : public A {
    virtual void hello() { ... }
};

Cách tiếp cận số 2:

struct B : public A {
    void hello() { ... }
};

Có sự khác biệt nào giữa hai cách này để ghi đè chức năng hello không?


65
Trong C ++ 11, bạn có thể viết "void hello () ghi đè {}" để tuyên bố rõ ràng rằng bạn đang ghi đè một phương thức ảo. Trình biên dịch sẽ thất bại nếu một phương thức ảo cơ sở không tồn tại và nó có khả năng đọc giống như đặt "ảo" trên lớp con cháu.
ShadowChaser

Trên thực tế, trong C ++ 11 của gcc, việc viết void hello () ghi đè {} trong lớp dẫn xuất là tốt vì lớp cơ sở đã chỉ định rằng phương thức hello () là ảo. Nói cách khác, việc sử dụng từ ảo trong lớp dẫn xuất là không cần thiết / bắt buộc, đối với gcc / g ++. (Tôi đang sử dụng gcc phiên bản 4.9.2 trên RPi 3) Nhưng dù sao cũng nên bao gồm từ khóa ảo trong phương thức của lớp dẫn xuất.
Sẽ

Câu trả lời:


183

Họ đều giống hệt nhau. Không có sự khác biệt giữa chúng ngoài cách tiếp cận đầu tiên đòi hỏi phải gõ nhiều hơn và có khả năng rõ ràng hơn.


25
Điều này là đúng, nhưng Hướng dẫn về tính di động của Mozilla C ++ khuyên bạn luôn luôn sử dụng ảo vì "một số trình biên dịch" sẽ đưa ra cảnh báo nếu bạn không. Quá tệ, họ không đề cập đến bất kỳ ví dụ về trình biên dịch như vậy.
Sergei Tachenov

5
Tôi cũng sẽ thêm rằng việc đánh dấu rõ ràng là ảo sẽ giúp nhắc nhở bạn làm cho hàm hủy là ảo.
lfalin

1
Chỉ đề cập đến, áp dụng tương tự cho kẻ hủy diệt ảo
Atul

6
@SergeyTachenov theo nhận xét của clifford cho câu trả lời của riêng anh ta , ví dụ về trình biên dịch như vậy là armcc.
Ruslan

4
@Rasmi, hướng dẫn tính di động mới có ở đây , nhưng bây giờ nó khuyên bạn nên sử dụng overridetừ khóa.
Sergei Tachenov

83

'Độ ảo' của một hàm được lan truyền ngầm, tuy nhiên ít nhất một trình biên dịch tôi sử dụng sẽ tạo ra một cảnh báo nếu virtualtừ khóa không được sử dụng rõ ràng, vì vậy bạn có thể muốn sử dụng nó nếu chỉ để giữ cho trình biên dịch im lặng.

Từ quan điểm hoàn toàn theo phong cách, bao gồm virtualtừ khóa rõ ràng 'quảng cáo' thực tế cho người dùng rằng chức năng này là ảo. Điều này sẽ rất quan trọng đối với bất kỳ ai phân loại B nữa mà không phải kiểm tra định nghĩa của A. Đối với hệ thống phân cấp lớp sâu, điều này trở nên đặc biệt quan trọng.


12
Trình biên dịch này là gì?
James McNellis

35
@James: armcc (trình biên dịch của ARM cho các thiết bị ARM)
Clifford

55

Các virtualtừ khóa là không cần thiết trong lớp dẫn xuất. Đây là tài liệu hỗ trợ, từ Tiêu chuẩn Dự thảo C ++ (N3337) (nhấn mạnh của tôi):

10.3 Chức năng ảo

2 Nếu một hàm thành viên ảo vfđược khai báo trong một lớp Basevà trong một lớp Derived, xuất phát trực tiếp hoặc gián tiếp từ Base, một hàm thành viên vfcó cùng tên, danh sách loại tham số (8.3.5), vòng loại cv và vòng loại ref ( hoặc không có cùng) như Base::vfđược khai báo, sau đó Derived::vfcũng là ảo ( cho dù nó có được khai báo hay không ) và nó ghi đè Base::vf.


5
Đây là câu trả lời tốt nhất ở đây.
Mr Mr tuyệt vời

33

Không, virtualtừ khóa trên chức năng ảo ghi đè của lớp dẫn xuất là không bắt buộc. Nhưng điều đáng nói là một cạm bẫy liên quan: lỗi không ghi đè chức năng ảo.

Các thất bại trong việc ghi đè xảy ra nếu bạn có ý định để ghi đè lên một hàm ảo trong một lớp học có nguồn gốc, nhưng thực hiện một lỗi trong chữ ký để nó tuyên bố một hàm ảo mới và khác nhau. Hàm này có thể là một sự quá tải của hàm lớp cơ sở, hoặc nó có thể khác nhau về tên. Cho dù bạn có sử dụng virtualtừ khóa trong khai báo hàm lớp dẫn xuất hay không , trình biên dịch sẽ không thể nói rằng bạn có ý định ghi đè một hàm từ một lớp cơ sở.

Tuy nhiên, cạm bẫy này được giải quyết một cách may mắn bởi tính năng ngôn ngữ ghi đè rõ ràng C ++ 11 , cho phép mã nguồn xác định rõ ràng rằng một hàm thành viên có ý định ghi đè chức năng lớp cơ sở:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

Trình biên dịch sẽ đưa ra lỗi thời gian biên dịch và lỗi lập trình sẽ rõ ràng ngay lập tức (có lẽ hàm trong Derured nên lấy floatlàm đối số).

Tham khảo WP: C ++ 11 .


11

Thêm từ khóa "ảo" là cách tốt để cải thiện khả năng đọc, nhưng không cần thiết. Các hàm được khai báo ảo trong lớp cơ sở và có cùng chữ ký trong các lớp dẫn xuất được coi là "ảo" theo mặc định.


7

Không có sự khác biệt cho trình biên dịch, khi bạn viết virtualtrong lớp dẫn xuất hoặc bỏ qua nó.

Nhưng bạn cần nhìn vào lớp cơ sở để có được thông tin này. Do đó, tôi khuyên bạn nên thêm virtualtừ khóa vào lớp dẫn xuất, nếu bạn muốn cho con người thấy rằng hàm này là ảo.


2

Có một sự khác biệt đáng kể khi bạn có các mẫu và bắt đầu lấy (các) lớp cơ sở làm (các) tham số mẫu:

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

Điều thú vị của nó là bây giờ bạn có thể định nghĩa các chức năng giao diện và phi giao diện sau này để xác định các lớp. Đó là hữu ích cho ảnh hưởng lẫn nhau giữa các giao diện thư viện (không dựa vào điều này như một quá trình thiết kế tiêu chuẩn của một đơn thư viện). Bạn không mất gì khi cho phép điều này cho tất cả các lớp học của bạn - thậm chí bạn có thể typedefB đến một cái gì đó nếu bạn muốn.

Lưu ý rằng, nếu bạn làm điều này, bạn cũng có thể muốn khai báo các công cụ sao chép / di chuyển dưới dạng mẫu: cho phép xây dựng từ các giao diện khác nhau cho phép bạn 'truyền' giữa các B<>loại khác nhau.

Đó là vấn đề cho dù bạn cần hỗ trợ thêm cho const A&trong t_hello(). Lý do thông thường cho việc viết lại này là để chuyển từ chuyên môn dựa trên thừa kế sang dựa trên mẫu, chủ yếu là vì lý do hiệu suất. Nếu bạn tiếp tục hỗ trợ giao diện cũ, bạn khó có thể phát hiện (hoặc ngăn chặn) việc sử dụng cũ.


1

Các virtualtừ khóa nên được bổ sung vào các chức năng của một lớp cơ sở để làm cho họ overridable. Trong ví dụ của bạn, struct Alà lớp cơ sở. virtualcó nghĩa là không có gì để sử dụng các hàm đó trong một lớp dẫn xuất. Tuy nhiên, bạn muốn lớp dẫn xuất của mình cũng là một lớp cơ sở và bạn muốn hàm đó có thể được ghi đè, thì bạn sẽ phải đặt virtualnó ở đó.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Ở đây Ckế thừa từ B, vì vậy Bkhông phải là lớp cơ sở (nó cũng là lớp dẫn xuất) và Clà lớp dẫn xuất. Sơ đồ thừa kế trông như thế này:

A
^
|
B
^
|
C

Vì vậy, bạn nên đặt virtualphía trước các chức năng bên trong các lớp cơ sở tiềm năng có thể có con. virtualcho phép con bạn ghi đè chức năng của bạn. Không có gì sai khi đặt virtualphía trước các hàm bên trong các lớp dẫn xuất, nhưng nó không bắt buộc. Mặc dù vậy, điều này được khuyến nghị, bởi vì nếu ai đó muốn thừa kế từ lớp dẫn xuất của bạn, họ sẽ không hài lòng rằng phương thức ghi đè không hoạt động như mong đợi.

Vì vậy, đặt virtualtrước các hàm trong tất cả các lớp liên quan đến thừa kế, trừ khi bạn biết chắc rằng lớp đó sẽ không có bất kỳ con nào cần ghi đè các hàm của lớp cơ sở. Đó là thực hành tốt.


0

Tôi chắc chắn sẽ bao gồm từ khóa ảo cho lớp con, bởi vì

  • Tôi. Dễ đọc.
  • ii. Lớp con này của tôi được dẫn xuất xuống sâu hơn, bạn không muốn hàm tạo của lớp dẫn xuất xa hơn gọi hàm ảo này.

1
Tôi nghĩ rằng anh ta có nghĩa là không đánh dấu chức năng con là ảo, một lập trình viên xuất phát từ lớp con sau này có thể không nhận ra rằng hàm thực sự là ảo (vì anh ta không bao giờ nhìn vào lớp cơ sở) và có thể gọi nó trong khi xây dựng ( mà có thể hoặc không thể làm điều đúng).
PfhorSlayer
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.