Tại sao một hàm bị ghi đè trong lớp dẫn xuất ẩn các quá tải khác của lớp cơ sở?


219

Hãy xem xét mã:

#include <stdio.h>

class Base {
public: 
    virtual void gogo(int a){
        printf(" Base :: gogo (int) \n");
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*) \n");
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*) \n");
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

Có lỗi này:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: Trong hàm `int main () ':
test.cpp: 31: error: không có chức năng phù hợp để gọi tới `Derured :: gogo (int) '
test.cpp: 21: lưu ý: các ứng cử viên là: virtual void Derogen :: gogo (int *) 
test.cpp: 33: 2: cảnh báo: không có dòng mới ở cuối tệp
> Mã thoát: 1

Ở đây, hàm của lớp Derogen làm lu mờ tất cả các hàm cùng tên (không phải chữ ký) trong lớp cơ sở. Bằng cách nào đó, hành vi này của C ++ có vẻ không ổn. Không đa hình.



8
câu hỏi tuyệt vời, tôi chỉ phát hiện ra điều này gần đây quá
Matt Joiner

11
Tôi nghĩ Bjarne (từ liên kết Mac đã đăng) đã đặt nó tốt nhất trong một câu: "Trong C ++, không có tình trạng quá tải trên các phạm vi - phạm vi lớp dẫn xuất không phải là ngoại lệ đối với quy tắc chung này."
sivabudh

7
@Ashish Liên kết đó bị hỏng. Đây là một cái đúng (tính đến thời điểm hiện tại) - stroustrup.com/bs_faq2.html#overloadderiving
nsane

3
Ngoài ra, muốn chỉ ra rằng obj.Base::gogo(7);vẫn hoạt động bằng cách gọi hàm ẩn.
đàn

Câu trả lời:


406

Đánh giá theo từ ngữ của câu hỏi của bạn (bạn đã sử dụng từ "ẩn"), bạn đã biết những gì đang xảy ra ở đây. Hiện tượng này được gọi là "giấu tên". Vì một số lý do, mỗi khi ai đó đặt câu hỏi về lý do tại sao việc ẩn tên xảy ra, những người trả lời lại nói rằng điều này được gọi là "ẩn tên" và giải thích cách hoạt động của nó (mà bạn có thể đã biết) hoặc giải thích cách ghi đè lên (mà bạn không bao giờ hỏi về), nhưng dường như không ai quan tâm để giải quyết câu hỏi "tại sao" thực sự.

Các quyết định, lý do đằng sau tên ẩn, tức là tại sao nó thực sự được thiết kế thành C ++, là để tránh một số hành vi phản trực giác, không lường trước và có khả năng nguy hiểm có thể xảy ra nếu tập hợp các hàm quá tải được thừa kế được phép trộn lẫn với tập hợp hiện tại của quá tải trong lớp nhất định. Bạn có thể biết rằng trong độ phân giải quá tải C ++ hoạt động bằng cách chọn chức năng tốt nhất từ ​​nhóm ứng cử viên. Điều này được thực hiện bằng cách khớp các loại đối số với các loại tham số. Các quy tắc khớp đôi khi có thể phức tạp và thường dẫn đến kết quả có thể được coi là phi logic bởi người dùng không chuẩn bị. Thêm các chức năng mới vào một tập hợp các chức năng hiện có trước đây có thể dẫn đến một sự thay đổi khá mạnh mẽ trong kết quả giải quyết quá tải.

Ví dụ, giả sử lớp cơ sở Bcó hàm thành viên foonhận tham số kiểu void *và tất cả các lệnh gọi foo(NULL)được giải quyết B::foo(void *). Giả sử không có tên ẩn và điều này B::foo(void *)có thể nhìn thấy trong nhiều lớp khác nhau giảm dần B. Tuy nhiên, giả sử trong một số hậu duệ [gián tiếp, từ xa] Dcủa lớp, Bmột hàm foo(int)được định nghĩa. Bây giờ, không có tên ẩn Dcó cả foo(void *)foo(int)có thể nhìn thấy và tham gia giải quyết quá tải. Hàm nào sẽ gọi để foo(NULL)giải quyết, nếu được thực hiện thông qua một đối tượng thuộc loại D? Họ sẽ giải quyết D::foo(int), vì intlà một kết hợp tốt hơn cho số không tích phân (nghĩa làNULL ) hơn bất kỳ loại con trỏ. Vì vậy, trong suốt các lệnh gọi phân cấp để foo(NULL)giải quyết một chức năng, trong khi trongD (và dưới), chúng đột nhiên giải quyết sang chức năng khác.

Một ví dụ khác được đưa ra trong Thiết kế và tiến hóa của C ++ , trang 77:

class Base {
    int x;
public:
    virtual void copy(Base* p) { x = p-> x; }
};

class Derived{
    int xx;
public:
    virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copy Base part of b
    b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}

Nếu không có quy tắc này, trạng thái của b sẽ được cập nhật một phần, dẫn đến cắt lát.

Hành vi này được coi là không mong muốn khi ngôn ngữ được thiết kế. Như một cách tiếp cận tốt hơn, nó đã được quyết định tuân theo đặc tả "ẩn tên", nghĩa là mỗi lớp bắt đầu bằng một "bảng sạch" đối với từng tên phương thức mà nó tuyên bố. Để ghi đè hành vi này, một hành động rõ ràng được yêu cầu từ người dùng: ban đầu là khai báo lại (các) phương thức được kế thừa (hiện không dùng nữa), giờ đây sử dụng khai báo rõ ràng.

Như bạn đã quan sát chính xác trong bài viết gốc của mình (tôi đang đề cập đến nhận xét "Không đa hình"), hành vi này có thể được coi là vi phạm quan hệ IS-A giữa các lớp. Điều này là đúng, nhưng rõ ràng hồi đó người ta đã quyết định rằng việc giấu tên cuối cùng sẽ chứng tỏ là một kẻ ác ít hơn.


22
Vâng, đây là một câu trả lời thực sự cho câu hỏi. Cảm ơn bạn. Tôi cũng tò mò.
Omnifarious

4
Câu trả lời chính xác! Ngoài ra, như một vấn đề thực tế, việc biên dịch có thể sẽ chậm hơn rất nhiều nếu việc tìm kiếm tên phải luôn luôn đi đến đỉnh cao.
Hội trường Drew

6
(Câu trả lời cũ, tôi biết.) Bây giờ nullptrtôi sẽ phản đối ví dụ của bạn bằng cách nói "nếu bạn muốn gọi void*phiên bản, bạn nên sử dụng một loại con trỏ". Có một ví dụ khác mà điều này có thể xấu?
GManNickG

3
Cái tên giấu giếm không thực sự xấu xa. Mối quan hệ "is-a" vẫn còn đó và có sẵn thông qua giao diện cơ sở. Vì vậy, có thể d->foo()sẽ không giúp bạn có được "Is-a Base", nhưng static_cast<Base*>(d)->foo() sẽ , bao gồm cả công văn động.
Kerrek SB

12
Câu trả lời này không có ích vì ví dụ đưa ra có cùng hoặc không che giấu: D :: foo (int) sẽ được gọi hoặc vì nó phù hợp hơn hoặc vì nó đã ẩn B: foo (int).
Richard Wolf

46

Các quy tắc phân giải tên nói rằng việc tra cứu tên dừng lại trong phạm vi đầu tiên tìm thấy tên phù hợp. Tại thời điểm đó, các quy tắc giải quyết quá tải bắt đầu để tìm ra sự phù hợp nhất của các chức năng có sẵn.

Trong trường hợp này, gogo(int*)được tìm thấy (một mình) trong phạm vi lớp Derogen và vì không có chuyển đổi tiêu chuẩn từ int sang int *, việc tra cứu thất bại.

Giải pháp là đưa các khai báo Cơ sở thông qua một khai báo sử dụng trong lớp Derogen:

using Base::gogo;

... sẽ cho phép các quy tắc tra cứu tên để tìm tất cả các ứng cử viên và do đó, việc giải quyết quá tải sẽ diễn ra như bạn mong đợi.


10
OP: "Tại sao một hàm bị ghi đè trong lớp dẫn xuất ẩn các quá tải khác của lớp cơ sở?" Câu trả lời này: "Bởi vì nó".
Richard Wolf

12

Đây là "Theo thiết kế". Trong độ phân giải quá tải C ++ cho loại phương thức này hoạt động như sau.

  • Bắt đầu từ loại tham chiếu và sau đó chuyển sang loại cơ sở, tìm loại đầu tiên có phương thức có tên là "gogo"
  • Chỉ xem xét các phương thức có tên "gogo" trên loại đó, tìm thấy sự quá tải phù hợp

Vì Derive không có chức năng khớp tên là "gogo", độ phân giải quá tải không thành công.


2

Việc ẩn tên có ý nghĩa bởi vì nó ngăn ngừa sự mơ hồ trong việc phân giải tên.

Xem xét mã này:

class Base
{
public:
    void func (float x) { ... }
}

class Derived: public Base
{
public:
    void func (double x) { ... }
}

Derived dobj;

Nếu Base::func(float)không bị ẩn Derived::func(double)trong Derogen, chúng ta sẽ gọi hàm lớp cơ sở khi gọi dobj.func(0.f), mặc dù số float có thể được tăng lên gấp đôi.

Tham khảo: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

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.