Chức năng ảo thuần túy với việc thực hiện


175

Hiểu biết cơ bản của tôi là không có triển khai cho một chức năng ảo thuần túy, tuy nhiên, tôi được cho biết có thể có triển khai cho chức năng ảo thuần túy.

class A {
public:
    virtual void f() = 0;
};

void A::f() {
    cout<<"Test"<<endl;
}

Mã ở trên có ổn không?

Mục đích để làm cho nó trở thành một chức năng ảo thuần túy với việc triển khai là gì?

Câu trả lời:


215

Một virtualhàm thuần túy phải được thực hiện theo kiểu dẫn xuất sẽ được khởi tạo trực tiếp, tuy nhiên kiểu cơ sở vẫn có thể định nghĩa một triển khai. Một lớp dẫn xuất có thể gọi một cách rõ ràng việc thực hiện lớp cơ sở (nếu quyền truy cập cho phép nó) bằng cách sử dụng một tên có phạm vi đầy đủ (bằng cách gọi A::f()trong ví dụ của bạn - nếu A::f()publichoặc protected). Cái gì đó như:

class B : public A {

    virtual void f() {
        // class B doesn't have anything special to do for f()
        //  so we'll call A's

        // note that A's declaration of f() would have to be public 
        //  or protected to avoid a compile time problem

        A::f();
    }

};

Trường hợp sử dụng mà tôi có thể nghĩ ra khỏi đỉnh đầu là khi có một hành vi mặc định hợp lý ít nhiều hợp lý, nhưng người thiết kế lớp muốn hành vi sắp xếp mặc định đó chỉ được gọi một cách rõ ràng. Nó cũng có thể là trường hợp bạn muốn các lớp dẫn xuất luôn thực hiện công việc của riêng chúng nhưng cũng có thể gọi một bộ chức năng chung.

Lưu ý rằng mặc dù ngôn ngữ cho phép, nhưng đó không phải là thứ mà tôi thấy thường được sử dụng (và thực tế là nó có thể được thực hiện dường như gây ngạc nhiên cho hầu hết các lập trình viên C ++, ngay cả những người có kinh nghiệm).


1
Bạn đã quên thêm lý do tại sao nó gây ngạc nhiên cho các lập trình viên: đó là vì định nghĩa nội tuyến bị cấm theo tiêu chuẩn. Các định nghĩa phương thức ảo thuần túy phải được deported. (trong một .inl hoặc .cpp để chỉ các thực hành đặt tên tệp phổ biến).
v.oddou

vì vậy phương thức gọi này giống như cách gọi thành viên phương thức tĩnh. Một số loại phương thức lớp trong Java.
Sany Liew

2
"không được sử dụng phổ biến" == thực hành xấu? Tôi đang tìm kiếm chính xác hành vi tương tự, cố gắng thực hiện NVI. Và NVI có vẻ là một thực hành tốt cho tôi.
Saskia

5
Thật đáng để chỉ ra rằng làm cho A :: f () thuần có nghĩa là B phải thực hiện f () (nếu không thì B sẽ trừu tượng và không thể chứng minh được). Và như @MichaelBurr chỉ ra, việc cung cấp một triển khai cho A :: f () có nghĩa là B có thể sử dụng nó để xác định f ().
sợ hãi_fool

2
IIRC, Scot Meyer có một bài viết tuyệt vời về trường hợp sử dụng câu hỏi này trong một trong những cuốn sách kinh điển của mình "C ++ hiệu quả hơn"
irsis

75

Để rõ ràng, bạn đang hiểu nhầm what = 0; sau một chức năng ảo có nghĩa là.

= 0 có nghĩa là các lớp dẫn xuất phải cung cấp một triển khai, không phải là lớp cơ sở không thể cung cấp một triển khai.

Trong thực tế, khi bạn đánh dấu một hàm ảo là thuần (= 0), có rất ít điểm trong việc cung cấp một định nghĩa, bởi vì nó sẽ không bao giờ được gọi trừ khi có ai đó làm như vậy thông qua Base :: Function (...) hoặc nếu Trình xây dựng lớp cơ sở gọi hàm ảo trong câu hỏi.


9
Điều này là không đúng. Nếu bạn gọi hàm ảo thuần túy đó tại hàm tạo của lớp ảo thuần của bạn, một cuộc gọi ảo thuần túy sẽ được thực hiện. Trong trường hợp bạn tốt hơn có một thực hiện.
rmn

@rmn, Có, bạn đúng về các cuộc gọi ảo trong các nhà xây dựng. Tôi cập nhật câu trả lời. Hy vọng rằng tất cả mọi người biết không làm điều đó, mặc dù. :)
Terry Mahaffey

3
Trong thực tế, thực hiện một cuộc gọi thuần cơ sở từ một nhà xây dựng dẫn đến hành vi được xác định thực hiện. Trong VC ++, đó là một sự cố _purecall.
Ofek Shilon

@OfekShilon là chính xác - Tôi cũng muốn gọi đó là hành vi không xác định và một ứng cử viên cho việc tái cấu trúc mã / thực hành xấu (tức là gọi các phương thức ảo bên trong hàm tạo). Tôi đoán nó có liên quan đến sự kết hợp bảng ảo, có thể không được chuẩn bị để định tuyến đến phần thân của việc thực hiện chính xác.
teodron

1
Trong các hàm tạo và hàm hủy, các hàm ảo không phải là ảo.
Jesper Juhl

20

Ưu điểm của nó là nó buộc các kiểu dẫn xuất vẫn ghi đè phương thức nhưng cũng cung cấp một triển khai mặc định hoặc phụ gia.


1
Tại sao tôi muốn buộc nếu có một triển khai mặc định? Nghe có vẻ như các chức năng ảo bình thường. Nếu đó chỉ là một chức năng ảo thông thường, tôi có thể ghi đè và nếu tôi không làm thì việc triển khai mặc định sẽ được cung cấp (triển khai cơ sở).
StackExchange123

19

Nếu bạn có mã nên được thực hiện bởi lớp phái sinh, nhưng bạn không muốn nó được thực thi trực tiếp - và bạn muốn buộc nó bị ghi đè.

Mã của bạn là chính xác, mặc dù tất cả trong tất cả điều này không phải là một tính năng thường được sử dụng và thường chỉ thấy khi cố gắng xác định một hàm hủy ảo thuần túy - trong trường hợp đó bạn phải cung cấp một triển khai. Điều buồn cười là một khi bạn xuất phát từ lớp đó, bạn không cần phải ghi đè lên hàm hủy.

Do đó, việc sử dụng hợp lý các hàm ảo thuần là xác định một hàm hủy ảo thuần là một từ khóa "không phải là cuối cùng".

Các mã sau đây là chính xác đáng ngạc nhiên:

class Base {
public:
  virtual ~Base() = 0;
};

Base::~Base() {}

class Derived : public Base {};

int main() { 
  // Base b; -- compile error
  Derived d; 
}

1
Các hàm hủy lớp cơ sở luôn được gọi bằng mọi cách, ảo hay không và thuần hoặc không; với các hàm khác, bạn không thể đảm bảo rằng một hàm ảo ghi đè sẽ gọi thực hiện lớp cơ sở cho dù phiên bản lớp cơ sở có thuần túy hay không.
CB Bailey

1
Mã đó là sai. Bạn phải định nghĩa dtor bên ngoài định nghĩa lớp do một cú pháp của ngôn ngữ.

@Roger: cảm ơn, điều đó thực sự đã giúp tôi - đây là mã tôi đã sử dụng, nó biên dịch tốt theo MSVC, nhưng tôi đoán nó sẽ không thể mang theo được.
Kornel Kisielewicz


4

Vâng cái này đúng rồi. Trong ví dụ của bạn, các lớp xuất phát từ A kế thừa cả giao diện f () và cách triển khai mặc định. Nhưng bạn buộc các lớp dẫn xuất thực hiện phương thức f () (ngay cả khi nó chỉ gọi cách thực hiện mặc định do A cung cấp).

Scott Meyers thảo luận về vấn đề này trong Hiệu quả C ++ (Phiên bản 2) Mục 36 Phân biệt giữa kế thừa giao diện và kế thừa thực hiện. Số mặt hàng có thể đã thay đổi trong phiên bản mới nhất.


4

Các hàm ảo thuần có hoặc không có phần thân đơn giản có nghĩa là các kiểu dẫn xuất phải cung cấp việc thực hiện riêng của chúng.

Các cơ quan chức năng ảo thuần túy trong lớp cơ sở rất hữu ích nếu các lớp dẫn xuất của bạn muốn gọi thực hiện lớp cơ sở của bạn.


2

'Khoảng trống ảo foo () = 0;' cú pháp không có nghĩa là bạn không thể triển khai foo () trong lớp hiện tại, bạn có thể. Nó cũng không có nghĩa là bạn phải thực hiện nó trong các lớp dẫn xuất . Trước khi bạn tát tôi, hãy quan sát vấn đề kim cương: (Mã ngầm định, để ý bạn).

class A
{
public: 
    virtual void foo()=0;
    virtual void bar();
}

class B : public virtual A
{
public:
    void foo() { bar(); }
}

class C : public virtual A
{
public:
    void bar();
}

class D : public B, public C
{}

int main(int argc, const char* argv[])
{
    A* obj = new D();
    **obj->foo();**
    return 0;
}

Bây giờ, lời gọi obj-> foo () sẽ dẫn đến B :: foo () và sau đó là C :: bar ().

Bạn thấy ... các phương thức ảo thuần túy không phải được triển khai trong các lớp dẫn xuất (foo () không có triển khai trong lớp C - trình biên dịch sẽ biên dịch) Trong C ++ có rất nhiều sơ hở.

Hy vọng tôi có thể giúp :-)


5
Nó không cần phải được thực hiện trong TẤT CẢ các lớp dẫn xuất, nhưng nó PHẢI có một triển khai trong tất cả các lớp dẫn xuất mà bạn dự định khởi tạo. Bạn không thể khởi tạo một đối tượng kiểu Ctrong ví dụ của bạn. Bạn có thể khởi tạo một đối tượng kiểu Dvì nó được triển khai footừ đó B.
YoungJohn

0

Một trường hợp sử dụng quan trọng của việc có một phương thức ảo thuần túy với một cơ quan triển khai , là khi bạn muốn có một lớp trừu tượng, nhưng bạn không có bất kỳ phương thức thích hợp nào trong lớp để biến nó thành ảo thuần. Trong trường hợp này, bạn có thể làm cho hàm hủy của lớp thuần ảo và đặt triển khai mong muốn của bạn (thậm chí là một phần thân trống) cho điều đó. Ví dụ:

class Foo
{
   virtual ~Foo() = 0;
   void bar1() {}
   void bar2(int x) {}
   // other methods
};

Foo::~Foo()
{
}

Kỹ thuật này, làm cho Foolớp trừu tượng và kết quả là không thể khởi tạo lớp trực tiếp. Đồng thời, bạn chưa thêm một phương thức ảo thuần túy nào để làm cho Foolớp trừu tượng.

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.