Lỗi "lệnh gọi hàm ảo thuần túy" đến từ đâu?


106

Đôi khi tôi nhận thấy các chương trình bị lỗi trên máy tính của mình với lỗi: "lệnh gọi hàm thuần ảo".

Làm thế nào để các chương trình này biên dịch ngay cả khi không thể tạo một đối tượng của một lớp trừu tượng?

Câu trả lời:


107

Chúng có thể dẫn đến kết quả nếu bạn cố gắng thực hiện một cuộc gọi hàm ảo từ một hàm tạo hoặc hàm hủy. Vì bạn không thể thực hiện một cuộc gọi hàm ảo từ một hàm tạo hoặc hàm hủy (đối tượng lớp dẫn xuất chưa được tạo hoặc đã bị phá hủy), nó sẽ gọi phiên bản lớp cơ sở, trong trường hợp là một hàm ảo thuần túy, thì không không tồn tại.

(Xem demo trực tiếp tại đây )

class Base
{
public:
    Base() { doIt(); }  // DON'T DO THIS
    virtual void doIt() = 0;
};

void Base::doIt()
{
    std::cout<<"Is it fine to call pure virtual function from constructor?";
}

class Derived : public Base
{
    void doIt() {}
};

int main(void)
{
    Derived d;  // This will cause "pure virtual function call" error
}

3
Nói chung, bất kỳ lý do nào khiến trình biên dịch không thể bắt được điều này?
Thomas

21
Trong trường hợp chung, không thể bắt nó vì luồng từ ctor có thể đi bất cứ đâu và bất cứ nơi nào có thể gọi hàm thuần ảo. Đây là sự cố đang tạm dừng 101.
shoosh 19/09/08

9
Câu trả lời hơi sai: một chức năng ảo thuần túy vẫn có thể được xác định, xem Wikipedia để biết thêm chi tiết. Đúng phân nhịp: có thể không tồn tại
MSalters

5
Tôi nghĩ rằng ví dụ này quá đơn giản: Lời doIt()gọi trong phương thức khởi tạo dễ dàng được tạo ra và gửi đến Base::doIt()tĩnh, điều này chỉ gây ra lỗi trình liên kết. Những gì chúng ta thực sự cần là một tình huống trong đó kiểu động trong quá trình điều phối động là kiểu cơ sở trừu tượng.
Kerrek SB

2
Điều này có thể được kích hoạt với MSVC nếu bạn thêm một mức điều hướng bổ sung: Base::Basegọi một phương thức không ảo f()mà lần lượt gọi doItphương thức ảo (thuần túy) .
Frerich Raabe

64

Cũng như trường hợp tiêu chuẩn của việc gọi một hàm ảo từ hàm tạo hoặc hàm hủy của một đối tượng với các hàm thuần ảo, bạn cũng có thể nhận được một lệnh gọi hàm thuần ảo (ít nhất là trên MSVC) nếu bạn gọi một hàm ảo sau khi đối tượng đã bị hủy. . Rõ ràng đây là một điều khá tệ để thử và làm nhưng nếu bạn đang làm việc với các lớp trừu tượng làm giao diện và bạn làm rối tung lên thì đó là điều bạn có thể thấy. Có thể xảy ra nhiều khả năng hơn nếu bạn đang sử dụng các giao diện được đếm tham chiếu và bạn gặp lỗi đếm số lượt tham khảo hoặc nếu bạn có điều kiện sử dụng đối tượng / cuộc đua phá hủy đối tượng trong một chương trình đa luồng ... Vấn đề về các loại cuộc gọi thuần túy này là nó thường ít dễ dàng hơn để xác định những gì đang xảy ra vì việc kiểm tra các 'nghi phạm thông thường' của các cuộc gọi ảo trong ctor và dtor sẽ trở nên sạch sẽ.

Để giúp gỡ lỗi các loại sự cố này, trong các phiên bản khác nhau của MSVC, bạn có thể thay thế trình xử lý cuộc gọi thuần túy của thư viện thời gian chạy. Bạn làm điều này bằng cách cung cấp chức năng của riêng bạn với chữ ký này:

int __cdecl _purecall(void)

và liên kết nó trước khi bạn liên kết thư viện thời gian chạy. Điều này cho phép BẠN kiểm soát những gì xảy ra khi một cuộc gọi thuần được phát hiện. Một khi bạn có quyền kiểm soát, bạn có thể làm điều gì đó hữu ích hơn trình xử lý tiêu chuẩn. Tôi có một trình xử lý có thể cung cấp dấu vết ngăn xếp về nơi mà cuộc gọi thuần đã xảy ra; xem tại đây: http://www.lenholgate.com/blog/2006/01/purecall.html để biết thêm chi tiết.

(Lưu ý rằng bạn cũng có thể gọi _set_purecall_handler () để cài đặt trình xử lý của bạn trong một số phiên bản MSVC).


1
Cảm ơn con trỏ về việc nhận lệnh gọi _purecall () trên một phiên bản đã xóa; Tôi không biết điều đó, nhưng chỉ chứng minh điều đó với bản thân mình bằng một đoạn mã thử nghiệm nhỏ. Nhìn vào bãi chứa sau khám nghiệm trong WinDbg, tôi nghĩ rằng tôi đang đối phó với một cuộc đua trong đó một luồng khác đang cố gắng sử dụng một đối tượng có nguồn gốc trước khi nó được xây dựng hoàn chỉnh, nhưng điều này đã chiếu một ánh sáng mới về vấn đề và có vẻ phù hợp hơn với bằng chứng.
Dave Ruske

1
Một điều khác mà tôi sẽ bổ sung: _purecall()lời gọi thường xảy ra khi gọi một phương thức của một cá thể đã xóa sẽ không xảy ra nếu lớp cơ sở đã được khai báo với __declspec(novtable)tối ưu hóa (cụ thể của Microsoft). Cùng với đó, bạn hoàn toàn có thể gọi một phương thức ảo bị ghi đè sau khi đối tượng đã bị xóa, điều này có thể che dấu sự cố cho đến khi nó cắn bạn ở một số dạng khác. Cái _purecall()bẫy là bạn của bạn!
Dave Ruske

Điều đó thật hữu ích khi biết Dave, gần đây tôi đã thấy một vài tình huống mà tôi không nhận được cuộc gọi thuần túy khi tôi nghĩ rằng tôi nên làm như vậy. Có lẽ tôi đã phạm lỗi tối ưu hóa đó.
Len Holgate

1
@LenHolgate: Câu trả lời vô cùng giá trị. Đây chính xác là trường hợp sự cố của chúng tôi (số lần giới thiệu sai do điều kiện chủng tộc). Cảm ơn bạn rất nhiều vì chỉ cho chúng tôi đi đúng hướng (chúng tôi đã nghi ngờ v-bảng tham nhũng thay vào đó và đi điên cố gắng để tìm mã thủ phạm)
BlueStrat

7

Thông thường khi bạn gọi một hàm ảo thông qua một con trỏ treo lơ lửng - rất có thể phiên bản đã bị hủy.

Cũng có thể có nhiều lý do "sáng tạo" hơn: có thể bạn đã quản lý để cắt bỏ phần đối tượng của mình nơi chức năng ảo được triển khai. Nhưng thường thì đối tượng đã bị phá hủy.


4

Tôi đã gặp phải tình huống rằng các hàm thuần ảo được gọi do các đối tượng bị phá hủy, Len Holgateđã có một câu trả lời rất hay , tôi muốn thêm một số màu với một ví dụ:

  1. Một đối tượng Derived được tạo và con trỏ (dưới dạng lớp Cơ sở) được lưu ở đâu đó
  2. Đối tượng Derived bị xóa, nhưng bằng cách nào đó, con trỏ vẫn được tham chiếu
  3. Con trỏ trỏ đến đối tượng Derived đã bị xóa được gọi

Bộ hủy lớp Derived đặt lại các điểm vptr thành vtable lớp Cơ sở, có chức năng ảo thuần túy, vì vậy khi chúng ta gọi hàm ảo, nó thực sự gọi vào các hàm thuần túy.

Điều này có thể xảy ra do một lỗi mã rõ ràng hoặc một kịch bản phức tạp về điều kiện chạy đua trong môi trường đa luồng.

Đây là một ví dụ đơn giản (biên dịch g ++ đã tắt tối ưu hóa - một chương trình đơn giản có thể dễ dàng tối ưu hóa):

 #include <iostream>
 using namespace std;

 char pool[256];

 struct Base
 {
     virtual void foo() = 0;
     virtual ~Base(){};
 };

 struct Derived: public Base
 {
     virtual void foo() override { cout <<"Derived::foo()" << endl;}
 };

 int main()
 {
     auto* pd = new (pool) Derived();
     Base* pb = pd;
     pd->~Derived();
     pb->foo();
 }

Và dấu vết ngăn xếp trông giống như:

#0  0x00007ffff7499428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff749b02a in __GI_abort () at abort.c:89
#2  0x00007ffff7ad78f7 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7adda46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7adda81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7ade84f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x0000000000400f82 in main () at purev.C:22

Điểm nổi bật:

nếu đối tượng bị xóa hoàn toàn, có nghĩa là hàm hủy được gọi và memroy được lấy lại, chúng ta có thể chỉ nhận được Segmentation faultvì bộ nhớ đã quay trở lại hệ điều hành và chương trình không thể truy cập nó. Vì vậy, kịch bản "cuộc gọi hàm thuần ảo" này thường xảy ra khi đối tượng được cấp phát trên vùng bộ nhớ, trong khi một đối tượng bị xóa, bộ nhớ bên dưới thực sự không được hệ điều hành lấy lại, nó vẫn có thể truy cập được bởi tiến trình.


0

Tôi đoán rằng có một vtbl được tạo cho lớp trừu tượng vì một số lý do nội bộ (nó có thể cần thiết cho một số loại thông tin loại thời gian chạy) và đã xảy ra sự cố và một đối tượng thực nhận được nó. Đó là một lỗi. Chỉ điều đó thôi đã nói lên rằng điều gì đó không thể xảy ra.

Đầu cơ thuần túy

chỉnh sửa: có vẻ như tôi đã sai trong trường hợp được đề cập. OTOH IIRC một số ngôn ngữ cho phép gọi vtbl ra khỏi hàm hủy của hàm tạo.


Nó không phải là một lỗi trong trình biên dịch, nếu đó là những gì bạn muốn nói.
Thomas

Sự nghi ngờ của bạn là đúng - C # và Java cho phép điều này. Trong những ngôn ngữ đó, các vật thể đang được xây dựng có kiểu cuối cùng. Trong C ++, các đối tượng thay đổi kiểu trong quá trình xây dựng và đó là lý do tại sao và khi nào bạn có thể có các đối tượng với kiểu trừu tượng.
MSalters

TẤT CẢ các lớp trừu tượng và các đối tượng thực được tạo ra từ chúng, cần một vtbl (bảng hàm ảo), liệt kê các hàm ảo nào nên được gọi trên nó. Trong C ++, một đối tượng chịu trách nhiệm tạo các thành viên của chính nó, bao gồm cả bảng hàm ảo. Các hàm tạo được gọi từ lớp cơ sở đến lớp dẫn xuất và các hàm hủy được gọi từ lớp dẫn xuất đến lớp cơ sở, vì vậy trong một lớp cơ sở trừu tượng, bảng hàm ảo chưa có sẵn.
mờTew

0

Tôi sử dụng VS2010 và bất cứ khi nào tôi thử gọi hàm hủy trực tiếp từ phương thức công khai, tôi gặp lỗi "lệnh gọi hàm thuần ảo" trong thời gian chạy.

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void SomeMethod1() { this->~Foo(); }; /* ERROR */
};

Vì vậy, tôi đã di chuyển những gì bên trong ~ Foo () sang phương thức private riêng biệt, sau đó nó hoạt động như một chiếc bùa.

template <typename T>
class Foo {
public:
  Foo<T>() {};
  ~Foo<T>() {};

public:
  void _MethodThatDestructs() {};
  void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};

0

Nếu bạn sử dụng Borland / CodeGear / Embarcadero / Idera C ++ Builder, bạn chỉ có thể triển khai

extern "C" void _RTLENTRY _pure_error_()
{
    //_ErrorExit("Pure virtual function called");
    throw Exception("Pure virtual function called");
}

Trong khi gỡ lỗi, hãy đặt một điểm ngắt trong mã và xem ngăn gọi trong IDE, nếu không thì ghi ngăn xếp cuộc gọi trong trình xử lý ngoại lệ của bạn (hoặc hàm đó) nếu bạn có công cụ thích hợp cho việc đó. Cá nhân tôi sử dụng MadExcept cho điều đó.

Tái bút. Lệnh gọi hàm ban đầu nằm trong [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp


-2

Đây là một cách lén lút để nó xảy ra. Tôi đã có điều này về cơ bản xảy ra với tôi ngày hôm nay.

class A
{
  A *pThis;
  public:
  A()
   : pThis(this)
  {
  }

  void callFoo()
  {
    pThis->foo(); // call through the pThis ptr which was initialized in the constructor
  }

  virtual void foo() = 0;
};

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

B b();
b.callFoo();

1
Ít nhất thì nó không thể được sao chép trên vc2008 của tôi, vptr trỏ đến vtable của A khi lần đầu tiên được khởi tạo trong contructor của A, nhưng sau đó khi B được khởi tạo hoàn toàn, vptr được thay đổi để trỏ đến vtable của B, điều này không sao cả
Baiyan Huang

coudnt tái tạo nó với vs2010 / 12
makc

I had this essentially happen to me todayrõ ràng là không đúng, vì đơn giản là sai: một hàm thuần ảo chỉ được gọi khi callFoo()được gọi bên trong một hàm tạo (hoặc hàm hủy), bởi vì tại thời điểm này đối tượng vẫn đang (hoặc đã) ở giai đoạn A. Đây là phiên bản mã của bạn đang chạy mà không có lỗi cú pháp trong B b();- dấu ngoặc đơn làm cho nó trở thành một khai báo hàm, bạn muốn một đối tượng.
Wolf
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.