Mục đích của từ khóa trên mạng cuối cùng trong C ++ 11 cho các chức năng là gì?


142

Mục đích của finaltừ khóa trong C ++ 11 cho các chức năng là gì? Tôi hiểu rằng nó ngăn chặn chức năng ghi đè bởi các lớp dẫn xuất, nhưng nếu đây là trường hợp, thì nó không đủ để tuyên bố là không ảo các finalchức năng của bạn ? Có điều gì khác tôi đang thiếu ở đây?


30
" Không đủ để tuyên bố là không ảo" các chức năng "cuối cùng" của bạn "Không, các chức năng ghi đè hoàn toàn là ảo cho dù bạn có sử dụng virtualtừ khóa hay không.
ildjarn

13
@ildjarn điều đó không đúng nếu họ không được tuyên bố là ảo trong siêu hạng, bạn không thể xuất phát từ một lớp và biến một phương thức không ảo thành một phương thức ảo ..
Dan O

10
@DanO tôi nghĩ bạn không thể ghi đè nhưng bạn có thể "ẩn" một phương thức theo cách đó .. điều này dẫn đến nhiều vấn đề vì mọi người không có ý định ẩn phương thức.
Alex Kremer

16
@DanO: Nếu nó không ảo trong siêu hạng thì nó sẽ không "ghi đè".
ildjarn

2
Một lần nữa, "ghi đè " có một ý nghĩa cụ thể ở đây, đó là đưa ra hành vi đa hình cho một hàm ảo. Trong ví dụ của bạn funckhông phải là ảo, vì vậy không có gì để ghi đè và do đó không có gì để đánh dấu là overridehoặc final.
ildjarn

Câu trả lời:


129

Điều bạn đang thiếu, vì idljarn đã được đề cập trong một nhận xét là nếu bạn ghi đè một hàm từ một lớp cơ sở, thì bạn không thể đánh dấu nó là không ảo:

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};

Cảm ơn! đây là điểm tôi còn thiếu: tức là ngay cả các lớp "lá" của bạn cũng cần đánh dấu chức năng của chúng là ảo ngay cả khi chúng có ý định ghi đè các hàm và không bị ghi đè
lezebulon

8
@lezebulon: Các lớp lá của bạn không cần đánh dấu một hàm là ảo nếu siêu lớp khai báo nó là ảo.
Dan O

5
Các phương thức trong các lớp lá là ảo hoàn toàn nếu chúng là ảo trong lớp cơ sở. Tôi nghĩ rằng trình biên dịch nên cảnh báo nếu điều này 'ảo' bị thiếu.
Aaron McDaid

@AaronMcDaid: Trình biên dịch thường cảnh báo về mã rằng, chính xác, có thể gây nhầm lẫn hoặc lỗi. Tôi chưa bao giờ thấy ai ngạc nhiên về tính năng đặc biệt này của ngôn ngữ theo cách có thể gây ra bất kỳ vấn đề nào, vì vậy tôi không thực sự biết lỗi đó có thể hữu ích như thế nào. Ngược lại, việc quên virtualcó thể gây ra lỗi và C ++ 11 đã thêm overridethẻ vào một chức năng sẽ phát hiện tình huống đó và không biên dịch khi một chức năng có nghĩa là ghi đè thực sự ẩn
David Rodríguez - lừa dối

1
Từ ghi chú thay đổi GCC 4.9: "Mô-đun phân tích thừa kế loại mới cải thiện tình trạng ảo hóa. Hiện tại ảo hóa có tính đến không gian tên ẩn danh và từ khóa cuối cùng của C ++ 11" - vì vậy, nó không chỉ là cú pháp cú pháp, mà còn có lợi ích tối ưu hóa tiềm năng.
kfsone

126
  • Đó là để ngăn chặn một lớp được kế thừa. Từ Wikipedia :

    C ++ 11 cũng bổ sung khả năng ngăn chặn kế thừa từ các lớp hoặc đơn giản là ngăn chặn các phương thức ghi đè trong các lớp dẫn xuất. Điều này được thực hiện với cuối cùng định danh đặc biệt. Ví dụ:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • Nó cũng được sử dụng để đánh dấu một hàm ảo để ngăn chặn nó bị ghi đè trong các lớp dẫn xuất:

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };

Wikipedia tiếp tục đưa ra một điểm thú vị :

Lưu ý rằng không phải overridecũng không phải finallà từ khóa ngôn ngữ. Họ là những định danh kỹ thuật; chúng chỉ đạt được ý nghĩa đặc biệt khi được sử dụng trong các bối cảnh cụ thể . Ở bất kỳ vị trí nào khác, chúng có thể là định danh hợp lệ.

Điều đó có nghĩa là, những điều sau đây được cho phép:

int const final = 0;     // ok
int const override = 1;  // ok

1
cảm ơn, nhưng tôi đã quên đề cập rằng câu hỏi của tôi liên quan đến việc sử dụng "cuối cùng" với các phương pháp
lezebulon

Bạn đã đề cập đến nó @lezebulon :-) "mục đích của từ khóa" cuối cùng "trong C ++ 11 cho các chức năng là gì ". (nhấn mạnh của tôi)
Aaron McDaid

Bạn đã chỉnh sửa nó? Tôi không thấy bất kỳ thông báo nào nói rằng "đã chỉnh sửa x phút trước bởi lezebulon". Làm thế nào điều đó xảy ra? Có lẽ bạn đã chỉnh sửa nó rất nhanh sau khi gửi nó?
Aaron McDaid

5
@Aaron: Chỉnh sửa được thực hiện trong vòng năm phút sau khi đăng không được phản ánh trong lịch sử sửa đổi.
ildjarn

@Nawaz: tại sao họ không chỉ là từ khóa? Có phải vì lý do tương thích có nghĩa là mã có sẵn trước khi C ++ 11 sử dụng cuối cùng & ghi đè cho các mục đích khác?
Kẻ hủy diệt

45

"Final" cũng cho phép tối ưu hóa trình biên dịch để bỏ qua cuộc gọi gián tiếp:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

với "cuối cùng", trình biên dịch có thể gọi CDerived::DoSomething()trực tiếp từ bên trong Blah()hoặc thậm chí là nội tuyến. Không có nó, nó phải tạo ra một cuộc gọi gián tiếp bên trong Blah()bởi vì Blah()có thể được gọi bên trong một lớp dẫn xuất đã bị ghi đè DoSomething().


29

Không có gì để thêm vào các khía cạnh ngữ nghĩa của "cuối cùng".

Nhưng tôi muốn thêm vào nhận xét của chris green rằng "cuối cùng" có thể trở thành một kỹ thuật tối ưu hóa trình biên dịch rất quan trọng trong tương lai không xa. Không chỉ trong trường hợp đơn giản mà ông đã đề cập, mà còn đối với các hệ thống phân cấp lớp thực tế phức tạp hơn có thể được "đóng" bằng "cuối cùng", do đó cho phép các trình biên dịch tạo mã gửi hiệu quả hơn so với cách tiếp cận vtable thông thường.

Một nhược điểm quan trọng của vtables là đối với bất kỳ đối tượng ảo nào như vậy (giả sử 64 bit trên CPU Intel điển hình), chỉ riêng con trỏ đã chiếm tới 25% (8 trong 64 byte) của một dòng bộ đệm. Trong các loại ứng dụng tôi thích viết, điều này rất đau. (Và theo kinh nghiệm của tôi, đó là đối số số 1 chống lại C ++ theo quan điểm hiệu suất thuần túy, tức là bởi các lập trình viên C.)

Trong các ứng dụng đòi hỏi hiệu năng cực cao, điều không quá bất thường đối với C ++, điều này thực sự có thể trở nên tuyệt vời, không yêu cầu khắc phục vấn đề này theo cách thủ công theo kiểu C hoặc tung hứng Mẫu lạ.

Kỹ thuật này được gọi là Devirtualization . Một thuật ngữ đáng nhớ. :-)

Có một bài phát biểu gần đây của Andrei Alexandrescu, giải thích khá rõ về cách bạn có thể giải quyết các tình huống như vậy ngày hôm nay và làm thế nào "cuối cùng" có thể là một phần của việc giải quyết các trường hợp tương tự "tự động" trong tương lai (thảo luận với người nghe):

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly


23
8 là 25% của 64?
ildjarn

6
Bất cứ ai cũng biết về một trình biên dịch sử dụng chúng bây giờ?
Vincent Fourmond

điều tương tự mà tôi muốn nói
crazii

8

Cuối cùng không thể được áp dụng cho các chức năng không ảo.

error: only virtual member functions can be marked 'final'

Sẽ không có ý nghĩa lắm khi có thể đánh dấu một phương thức không ảo là 'cuối cùng'. Được

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()sẽ luôn gọi A::foo.

Nhưng, nếu A :: foo là virtual, thì B :: foo sẽ ghi đè lên nó. Điều này có thể là không mong muốn, và do đó sẽ có ý nghĩa để làm cho chức năng ảo cuối cùng.

Câu hỏi là mặc dù, tại sao cho phép cuối cùng trên các chức năng ảo. Nếu bạn có một hệ thống phân cấp sâu sắc:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

Sau đó, việc finalđặt một "tầng" về mức độ ghi đè có thể được thực hiện. Các lớp khác có thể mở rộng A và B và ghi đè lên chúng foo, nhưng lớp này mở rộng C thì không được phép.

Vì vậy, nó có thể không có ý nghĩa để làm cho foo 'cấp cao nhất' final, nhưng nó có thể có ý nghĩa thấp hơn.

(Tôi nghĩ mặc dù, có chỗ để mở rộng các từ cuối cùng và ghi đè lên các thành viên không ảo. Mặc dù vậy, chúng sẽ có một ý nghĩa khác.)


cảm ơn vì ví dụ, đó là điều mà tôi không chắc chắn. Nhưng vẫn còn: điểm để có một chức năng cuối cùng (và ảo) là gì? Về cơ bản, bạn sẽ không bao giờ có thể sử dụng thực tế là chức năng này là ảo vì nó không thể bị ghi đè
lezebulon

@lezebulon, tôi đã chỉnh sửa câu hỏi của tôi. Nhưng sau đó tôi nhận thấy câu trả lời của DanO - đó là một câu trả lời rõ ràng tốt về những gì tôi đang cố nói.
Aaron McDaid

Tôi không phải là một chuyên gia, nhưng tôi cảm thấy rằng đôi khi nó có thể có ý nghĩa để tạo ra một chức năng cấp cao nhất final. Ví dụ, nếu bạn biết rằng bạn muốn tất cả các Shapes đến foo()một cái gì đó được xác định trước và xác định rằng không có hình dạng dẫn xuất nào phải sửa đổi. Hoặc, tôi đã sai và có một mô hình tốt hơn để sử dụng cho trường hợp đó? EDIT: Ồ, có lẽ bởi vì trong trường hợp đó, người ta không nên bắt đầu cấp cao nhất foo() virtual? Tuy nhiên, nó vẫn có thể bị ẩn, ngay cả khi được gọi chính xác (đa hình) thông qua Shape*...
Andrew Cheong

8

Một trường hợp sử dụng cho từ khóa 'cuối cùng' mà tôi thích là như sau:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};

1
Có, đây thực chất là một ví dụ về Mẫu Phương thức Mẫu. Và trước C ++ 11, luôn là TMP khiến tôi ước rằng C ++ có một tính năng ngôn ngữ như là Final Final, như Java đã làm.
Kaitain

6

final thêm một ý định rõ ràng để không bị ghi đè chức năng của bạn và sẽ gây ra lỗi trình biên dịch nếu điều này bị vi phạm:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

Khi mã đứng, nó biên dịch và B::fooghi đè A::foo. B::fooNhân tiện cũng là ảo. Tuy nhiên, nếu chúng ta thay đổi # 1 thành virtual int foo() final, thì đây là lỗi trình biên dịch và chúng ta không được phép ghi đè A::foothêm trong các lớp dẫn xuất.

Lưu ý rằng điều này không cho phép chúng tôi "mở lại" một hệ thống phân cấp mới, tức là không có cách nào để tạo B::foomột chức năng mới, không liên quan có thể độc lập ở phần đầu của hệ thống phân cấp ảo mới. Khi một hàm là cuối cùng, nó không bao giờ có thể được khai báo lại trong bất kỳ lớp dẫn xuất nào.


5

Từ khóa cuối cùng cho phép bạn khai báo một phương thức ảo, ghi đè lên N lần và sau đó bắt buộc rằng 'điều này không còn có thể bị ghi đè'. Nó sẽ hữu ích trong việc hạn chế sử dụng lớp dẫn xuất của bạn, để bạn có thể nói "Tôi biết siêu hạng của tôi cho phép bạn ghi đè lên điều này, nhưng nếu bạn muốn xuất phát từ tôi, bạn không thể!".

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

Như các áp phích khác đã chỉ ra, nó không thể được áp dụng cho các chức năng không ảo.

Một mục đích của từ khóa cuối cùng là để ngăn chặn tình cờ ghi đè một phương thức. Trong ví dụ của tôi, DoStuff () có thể là một hàm trợ giúp mà lớp dẫn xuất chỉ cần đổi tên để có được hành vi đúng. Nếu không có cuối cùng, lỗi sẽ không được phát hiện cho đến khi thử nghiệm.


1

Từ khóa cuối cùng trong C ++ khi được thêm vào một hàm, ngăn không cho nó bị ghi đè bởi một lớp cơ sở. Ngoài ra khi thêm vào một lớp ngăn chặn sự kế thừa của bất kỳ loại nào. Xem xét ví dụ sau đây cho thấy việc sử dụng công cụ xác định cuối cùng. Chương trình này thất bại trong việc biên dịch.

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

Cũng thế:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}

0

Bổ sung cho câu trả lời của Mario Knezović:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

Đoạn mã trên cho thấy lý thuyết, nhưng không thực sự được thử nghiệm trên các trình biên dịch thực. Rất nhiều đánh giá cao nếu bất cứ ai dán một đầu ra tháo rờ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.