Đô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?
Đô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:
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
}
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.
Base::Base
gọi một phương thức không ảo f()
mà lần lượt gọi doIt
phương thức ảo (thuần túy) .
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).
_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!
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ụ:
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 fault
vì 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.
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.
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 */
};
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
Đâ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();
I had this essentially happen to me today
rõ 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.