Trong C ++, khi nào tôi nên sử dụng cuối cùng trong khai báo phương thức ảo?


11

Tôi biết rằng finaltừ khóa được sử dụng để ngăn chặn phương thức ảo bị ghi đè bởi các lớp dẫn xuất. Tuy nhiên, tôi không thể tìm thấy bất kỳ ví dụ hữu ích nào khi tôi thực sự nên sử dụng finaltừ khóa với virtualphương pháp. Thậm chí, có cảm giác như việc sử dụng các finalphương thức ảo là một mùi khó chịu vì nó không cho phép các lập trình viên mở rộng lớp học trong tương lai.

Câu hỏi của tôi là tiếp theo:

Có trường hợp hữu ích nào khi tôi thực sự nên sử dụng finaltrong virtualkhai báo phương thức không?


Đối với tất cả các lý do tương tự mà các phương thức Java được thực hiện cuối cùng?
Ngày

Trong Java tất cả các phương thức là ảo. Phương pháp cuối cùng trong lớp cơ sở có thể cải thiện hiệu suất. C ++ có các phương thức không ảo, vì vậy đây không phải là trường hợp của ngôn ngữ này. Bạn có biết trường hợp tốt nào khác không? Tôi muốn nghe họ. :)
metamaker

Đoán tôi không giỏi giải thích mọi thứ - tôi đã cố nói chính xác như Mike Nakis.
Ngày

Câu trả lời:


12

Một bản tóm tắt về những gì finaltừ khóa làm: giả sử chúng ta có lớp cơ sở Avà lớp dẫn xuất B. Hàm f()có thể được khai báo Alà as virtual, có nghĩa là lớp Bcó thể ghi đè lên nó. Nhưng sau đó lớp Bcó thể muốn rằng bất kỳ lớp nào có nguồn gốc hơn nữa Bsẽ không thể ghi đè f(). Đó là khi chúng ta cần khai báo f()như finaltrong B. Không có finaltừ khóa, một khi chúng ta định nghĩa một phương thức là ảo, bất kỳ lớp dẫn xuất nào cũng có thể tự do ghi đè lên nó. Các finaltừ khóa được sử dụng để đặt dấu chấm hết cho sự tự do này.

Một ví dụ về lý do tại sao bạn cần một thứ như vậy: Giả sử lớp Ađịnh nghĩa một hàm ảo prepareEnvelope()và lớp Bghi đè lên nó và thực hiện nó như một chuỗi các cuộc gọi đến các phương thức ảo của chính nó stuffEnvelope(), lickEnvelope()sealEnvelope(). Lớp Bdự định cho phép các lớp dẫn xuất ghi đè các phương thức ảo này để cung cấp các triển khai riêng của chúng, nhưng lớp Bkhông muốn cho phép bất kỳ lớp dẫn xuất nào ghi đè prepareEnvelope()và do đó thay đổi thứ tự của các công cụ, liếm, đóng dấu hoặc bỏ qua việc gọi một trong số chúng. Vì vậy, trong trường hợp này lớp Btuyên bố prepareEnvelope()là cuối cùng.


Đầu tiên, cảm ơn bạn đã trả lời nhưng không phải là những gì tôi đã viết trong câu đầu tiên của câu hỏi? Những gì tôi thực sự cần là một trường hợp khi class B might wish that any class which is further derived from B should not be able to override f()? Có trường hợp thế giới thực nào mà lớp B và phương thức f () như vậy tồn tại và phù hợp không?
metamaker 7/07/2015

Vâng, tôi xin lỗi, chờ đợi, tôi đang viết một ví dụ ngay bây giờ.
Mike Nakis

@metamaker có bạn đi.
Mike Nakis

1
Ví dụ thực sự tốt, đây là câu trả lời tốt nhất cho đến nay. Cám ơn!
metamaker 7/07/2015

1
Sau đó cảm ơn bạn đã lãng phí thời gian. Tôi không thể tìm thấy một ví dụ hay trên Internet, đã lãng phí rất nhiều thời gian để tìm kiếm. Tôi sẽ đặt +1 cho câu trả lời nhưng tôi không thể làm điều đó vì tôi có uy tín thấp. :( Có thể sau này.
metamaker 7/07/2015

2

Nó thường hữu ích từ góc độ thiết kế để có thể đánh dấu mọi thứ là không thay đổi. Theo cách tương tự, bộ constcung cấp trình bảo vệ trình biên dịch và chỉ ra rằng trạng thái không nên thay đổi, finalcó thể được sử dụng để chỉ ra rằng hành vi không nên thay đổi bất kỳ phân cấp kế thừa nào nữa.

Thí dụ

Hãy xem xét một trò chơi video trong đó các phương tiện đưa người chơi từ vị trí này sang vị trí khác. Tất cả các phương tiện nên kiểm tra để đảm bảo rằng họ đang di chuyển đến một địa điểm hợp lệ trước khi khởi hành (đảm bảo căn cứ tại địa điểm đó không bị phá hủy, ví dụ). Chúng ta có thể bắt đầu sử dụng thành ngữ giao diện không ảo (NVI) để đảm bảo rằng kiểm tra này được thực hiện bất kể phương tiện.

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

Bây giờ hãy nói rằng chúng ta có phương tiện bay trong trò chơi của chúng ta, và một điều mà tất cả các phương tiện bay yêu cầu và có điểm chung là chúng phải trải qua kiểm tra an toàn bên trong nhà chứa máy bay trước khi cất cánh.

Ở đây chúng ta có thể sử dụng finalđể đảm bảo rằng tất cả các phương tiện bay sẽ trải qua quá trình kiểm tra như vậy và cũng truyền đạt yêu cầu thiết kế này của các phương tiện bay.

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

Bằng cách sử dụng finaltheo cách này, chúng tôi sắp xếp một cách hiệu quả tính linh hoạt của thành ngữ giao diện không ảo để cung cấp hành vi thống nhất xuống hệ thống phân cấp thừa kế (ngay cả khi suy nghĩ lại, chống lại vấn đề lớp cơ sở mong manh) cho chính các hàm ảo. Hơn nữa, chúng tôi tự mua phòng ngọ nguậy để thực hiện các thay đổi trung tâm ảnh hưởng đến tất cả các loại phương tiện bay như một cách suy nghĩ mà không sửa đổi từng triển khai phương tiện bay đang tồn tại.

Đây là một ví dụ như vậy của việc sử dụng final. Có những bối cảnh bạn sẽ gặp phải khi đơn giản là nó không có ý nghĩa gì khi chức năng thành viên ảo bị ghi đè thêm nữa - làm như vậy có thể dẫn đến một thiết kế dễ vỡ và vi phạm các yêu cầu thiết kế của bạn.

Đó là nơi finalhữu ích từ quan điểm thiết kế / kiến ​​trúc.

Nó cũng hữu ích từ quan điểm của trình tối ưu hóa vì nó cung cấp cho trình tối ưu hóa thông tin thiết kế này cho phép nó làm mờ các cuộc gọi chức năng ảo (loại bỏ chi phí gửi động, và thường là đáng kể hơn, loại bỏ rào cản tối ưu hóa giữa người gọi và callee).

Câu hỏi

Từ các ý kiến:

Tại sao cuối cùng và ảo sẽ được sử dụng cùng một lúc?

Nó không có nghĩa đối với một lớp cơ sở ở gốc của một hệ thống phân cấp để khai báo một hàm là cả hai virtualfinal. Điều đó có vẻ khá ngớ ngẩn đối với tôi, vì nó sẽ khiến cả trình biên dịch và người đọc phải nhảy qua các vòng không cần thiết có thể tránh được bằng cách đơn giản là tránh virtualhoàn toàn trong trường hợp như vậy. Tuy nhiên, các lớp con kế thừa các hàm thành viên ảo như vậy:

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

Trong trường hợp này, có hay không Bar::fsử dụng từ khóa ảo một cách rõ ràng, Bar::flà một chức năng ảo. Các virtualtừ khóa sau đó trở thành tùy chọn trong trường hợp này. Vì vậy, nó có thể có ý nghĩa cho Bar::fđược quy định như final, mặc dù nó là một hàm ảo ( finalcó thể chỉ được sử dụng cho các chức năng ảo).

Và một số người có thể thích, theo phong cách, để chỉ rõ rằng đó Bar::flà ảo, như vậy:

struct Bar: Foo
{
   virtual void f() final {...}
};

Đối với tôi, việc sử dụng cả hai virtualfinalchỉ định cho cùng một chức năng trong bối cảnh này (tương tự virtualoverride) là một vấn đề dư thừa , nhưng đó là vấn đề về phong cách trong trường hợp này. Một số người có thể thấy rằng virtualtruyền đạt một cái gì đó có giá trị ở đây, giống như sử dụng externđể khai báo hàm với liên kết ngoài (mặc dù tùy chọn này thiếu các vòng loại liên kết khác).


Làm thế nào để bạn có kế hoạch ghi đè privatephương thức trong Vehiclelớp của bạn ? Ý bạn không phải là protectedthay vào đó?
Andy

3
@DavidPacker privatekhông mở rộng để ghi đè (một chút phản trực giác). Công cụ xác định công khai / được bảo vệ / riêng tư cho các chức năng ảo chỉ áp dụng cho người gọi và không áp dụng cho bộ ghi đè, đặt một cách thô bạo. Một lớp dẫn xuất có thể ghi đè các hàm ảo từ lớp cơ sở của nó bất kể mức độ hiển thị của nó.

@DavidPacker protectedcó thể có ý nghĩa trực quan hơn một chút. Tôi chỉ muốn đạt được tầm nhìn thấp nhất có thể bất cứ khi nào tôi có thể. Tôi nghĩ lý do ngôn ngữ được thiết kế theo cách này là vì, nếu không, các chức năng thành viên ảo riêng sẽ không có ý nghĩa gì ngoài bối cảnh của tình bạn, vì không có lớp nào ngoài một người bạn sau đó có thể ghi đè lên chúng nếu các công cụ truy cập quan trọng trong ngữ cảnh ghi đè và không chỉ gọi.

Tôi nghĩ rằng nó được đặt thành riêng tư thậm chí sẽ không biên dịch. Ví dụ, cả Java và C # đều không cho phép điều đó. C ++ làm tôi ngạc nhiên mỗi ngày. Ngay cả sau 10 năm lập trình.
Andy

@DavidPacker Nó cũng làm tôi vấp ngã khi lần đầu tiên gặp nó. Có lẽ tôi chỉ nên làm cho nó protectedđể tránh nhầm lẫn người khác. Cuối cùng tôi đã đưa ra một bình luận, ít nhất, mô tả làm thế nào các chức năng ảo riêng tư vẫn có thể bị ghi đè.

0
  1. Nó cho phép rất nhiều tối ưu hóa, bởi vì nó có thể được biết tại thời điểm biên dịch mà hàm được gọi.

  2. Hãy cẩn thận ném từ "mùi mã" xung quanh. "Chung kết" không làm cho việc mở rộng lớp học là không thể. Nhấp đúp chuột vào từ "cuối cùng", nhấn backspace và mở rộng lớp. TUYỆT VỜI cuối cùng là một tài liệu tuyệt vời mà nhà phát triển không mong đợi bạn ghi đè chức năng và vì điều đó, nhà phát triển tiếp theo nên rất cẩn thận, vì lớp có thể ngừng hoạt động đúng nếu phương thức cuối cùng bị ghi đè theo cách đó.


Tại sao sẽ finalvirtualbao giờ được sử dụng cùng một lúc?
Robert Harvey

1
Nó cho phép rất nhiều tối ưu hóa, bởi vì nó có thể được biết tại thời điểm biên dịch mà hàm được gọi. Bạn có thể giải thích làm thế nào hoặc cung cấp một tài liệu tham khảo? TUYỆT VỜI cuối cùng là một tài liệu tuyệt vời mà nhà phát triển không mong đợi bạn ghi đè chức năng và vì điều đó, nhà phát triển tiếp theo nên rất cẩn thận, vì lớp có thể ngừng hoạt động đúng nếu phương thức cuối cùng bị ghi đè theo cách đó. Đó không phải là một mùi hôi?
siêu nhân

@RobertHarvey ABI ổn định. Trong mọi trường hợp khác, nó có lẽ nên finaloverride.
Ded repeatator

@metamaker tôi đã cùng Q, thảo luận trong bình luận ở đây - programmers.stackexchange.com/questions/256778/... - & hoạt kê đã vấp vào một số cuộc thảo luận khác, chỉ vì yêu! Về cơ bản, đó là về việc nếu một trình biên dịch / trình liên kết tối ưu hóa có thể xác định liệu một tham chiếu / con trỏ có thể đại diện cho một lớp dẫn xuất hay không và do đó cần một cuộc gọi ảo. Nếu phương thức (hoặc lớp) có thể được ghi đè vượt ra ngoài loại tĩnh của tham chiếu, chúng có thể sai lệch: gọi trực tiếp. Nếu có bất kỳ nghi ngờ nào - như thường lệ - họ phải gọi hầu như. Tuyên bố finalthực sự có thể giúp họ
gạch dưới
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.