บางครั้งฉันสังเกตเห็นโปรแกรมที่ขัดข้องในคอมพิวเตอร์ของฉันโดยมีข้อผิดพลาด: "pure virtual function call"
โปรแกรมเหล่านี้จะคอมไพล์ได้อย่างไรเมื่อวัตถุไม่สามารถสร้างคลาสนามธรรมได้
บางครั้งฉันสังเกตเห็นโปรแกรมที่ขัดข้องในคอมพิวเตอร์ของฉันโดยมีข้อผิดพลาด: "pure virtual function call"
โปรแกรมเหล่านี้จะคอมไพล์ได้อย่างไรเมื่อวัตถุไม่สามารถสร้างคลาสนามธรรมได้
คำตอบ:
ซึ่งอาจเกิดขึ้นได้หากคุณพยายามเรียกใช้ฟังก์ชันเสมือนจากตัวสร้างหรือตัวทำลาย เนื่องจากคุณไม่สามารถเรียกใช้ฟังก์ชันเสมือนจากคอนสตรัคเตอร์หรือตัวทำลาย (อ็อบเจ็กต์คลาสที่ได้รับยังไม่ได้ถูกสร้างขึ้นหรือถูกทำลายไปแล้ว) จึงเรียกเวอร์ชันคลาสพื้นฐานซึ่งในกรณีของฟังก์ชันเสมือนจริงจะไม่ ไม่มีอยู่จริง
(ดูการสาธิตสดที่นี่ )
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()
เรียกในตัวสร้างนั้นเบี่ยงเบนได้ง่ายและถูกส่งไปยังBase::doIt()
แบบคงที่ซึ่งทำให้เกิดข้อผิดพลาดตัวเชื่อมโยง สิ่งที่เราต้องการจริงๆคือสถานการณ์ที่ประเภทไดนามิกในระหว่างการจัดส่งแบบไดนามิกคือประเภทฐานนามธรรม
Base::Base
เรียก non-virtual f()
ซึ่งจะเรียกdoIt
วิธีการเสมือน (pure)
เช่นเดียวกับกรณีมาตรฐานของการเรียกใช้ฟังก์ชันเสมือนจากตัวสร้างหรือตัวทำลายของวัตถุด้วยฟังก์ชันเสมือนจริงคุณยังสามารถเรียกใช้ฟังก์ชันเสมือนจริงได้ (บน MSVC เป็นอย่างน้อย) หากคุณเรียกฟังก์ชันเสมือนหลังจากที่วัตถุถูกทำลาย . เห็นได้ชัดว่านี่เป็นสิ่งที่ไม่ดีเลยที่จะลองทำ แต่ถ้าคุณทำงานกับคลาสนามธรรมเป็นอินเทอร์เฟซและคุณทำผิดพลาดคุณอาจเห็น อาจเป็นไปได้มากขึ้นหากคุณใช้อินเทอร์เฟซที่นับอ้างอิงและคุณมีข้อผิดพลาดในการนับการอ้างอิงหรือหากคุณมีเงื่อนไขการแข่งขันการใช้อ็อบเจ็กต์ / การทำลายอ็อบเจ็กต์ในโปรแกรมมัลติเธรด ... สิ่งที่เกี่ยวกับ purecall ประเภทนี้ก็คือ มักจะไม่ค่อยง่ายนักที่จะเข้าใจว่าเกิดอะไรขึ้นเนื่องจากการตรวจสอบ 'ผู้ต้องสงสัยตามปกติ' ของการโทรเสมือนใน ctor และ dtor จะเกิดความสะอาด
เพื่อช่วยในการดีบักปัญหาเหล่านี้คุณสามารถทำได้ใน MSVC เวอร์ชันต่างๆให้แทนที่ตัวจัดการ purecall ของไลบรารีรันไทม์ คุณทำได้โดยระบุฟังก์ชันของคุณเองพร้อมลายเซ็นนี้:
int __cdecl _purecall(void)
และเชื่อมโยงก่อนที่คุณจะเชื่อมโยงไลบรารีรันไทม์ สิ่งนี้ช่วยให้คุณสามารถควบคุมสิ่งที่เกิดขึ้นเมื่อตรวจพบการโทร purecall เมื่อคุณควบคุมได้แล้วคุณสามารถทำสิ่งที่มีประโยชน์มากกว่าตัวจัดการมาตรฐาน ฉันมีตัวจัดการที่สามารถจัดเตรียมสแต็กแทร็กว่า purecall เกิดขึ้นที่ไหน ดูที่นี่: http://www.lenholgate.com/blog/2006/01/purecall.htmlสำหรับรายละเอียดเพิ่มเติม
(โปรดทราบว่าคุณสามารถเรียกใช้ _set_purecall_handler () เพื่อติดตั้งตัวจัดการของคุณใน MSVC บางเวอร์ชัน)
_purecall()
เรียกใช้ที่ปกติเกิดขึ้นในการเรียกเมธอดของอินสแตนซ์ที่ถูกลบจะไม่เกิดขึ้นหากคลาสพื้นฐานได้รับการประกาศด้วยการปรับให้__declspec(novtable)
เหมาะสม (เฉพาะของ Microsoft) ด้วยเหตุนี้จึงเป็นไปได้ทั้งหมดที่จะเรียกวิธีการเสมือนจริงที่ถูกแทนที่หลังจากที่วัตถุถูกลบไปแล้วซึ่งอาจปกปิดปัญหาจนกว่ามันจะกัดคุณในรูปแบบอื่น _purecall()
กับดักเป็นเพื่อนของคุณ!
โดยปกติเมื่อคุณเรียกใช้ฟังก์ชันเสมือนผ่านตัวชี้ห้อยซึ่งส่วนใหญ่แล้วอินสแตนซ์จะถูกทำลายไปแล้ว
อาจมีเหตุผลที่ "สร้างสรรค์" มากขึ้นเช่นกัน: บางทีคุณอาจจัดการตัดส่วนของวัตถุที่ใช้ฟังก์ชันเสมือน แต่โดยปกติแล้วอินสแตนซ์นั้นถูกทำลายไปแล้ว
ฉันพบสถานการณ์ที่ฟังก์ชันเสมือนจริงถูกเรียกใช้เนื่องจากวัตถุที่ถูกทำลายLen Holgate
มีคำตอบที่ดีมากอยู่แล้วฉันต้องการเพิ่มสีด้วยตัวอย่าง:
ตัวทำลายคลาสที่ได้รับจะรีเซ็ตจุด vptr ไปที่ฐานคลาส vtable ซึ่งมีฟังก์ชันเสมือนจริงดังนั้นเมื่อเราเรียกใช้ฟังก์ชันเสมือนจริงมันจะเรียกไปยังฟังก์ชันที่บริสุทธิ์
สิ่งนี้อาจเกิดขึ้นเนื่องจากข้อผิดพลาดของโค้ดที่ชัดเจนหรือสถานการณ์ที่ซับซ้อนของสภาพการแข่งขันในสภาพแวดล้อมแบบมัลติเธรด
นี่คือตัวอย่างง่ายๆ (การคอมไพล์ g ++ โดยปิดการปรับให้เหมาะสม - โปรแกรมง่ายๆสามารถปรับให้เหมาะสมได้อย่างง่ายดาย):
#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();
}
และการติดตามสแต็กดูเหมือนว่า:
#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
ไฮไลต์:
หากอ็อบเจ็กต์ถูกลบอย่างสมบูรณ์หมายความว่า destructor ถูกเรียกและ memroy ถูกเรียกคืนเราอาจได้รับ a Segmentation fault
เมื่อหน่วยความจำกลับสู่ระบบปฏิบัติการและโปรแกรมก็ไม่สามารถเข้าถึงได้ ดังนั้นสถานการณ์ "การเรียกใช้ฟังก์ชันเสมือนจริง" นี้มักจะเกิดขึ้นเมื่อวัตถุถูกจัดสรรบนพูลหน่วยความจำในขณะที่วัตถุถูกลบไปหน่วยความจำพื้นฐานจะไม่ถูกเรียกคืนโดยระบบปฏิบัติการ แต่กระบวนการนี้ยังสามารถเข้าถึงได้
ฉันเดาว่ามี vtbl ที่สร้างขึ้นสำหรับคลาสนามธรรมด้วยเหตุผลภายในบางอย่าง (อาจจำเป็นสำหรับข้อมูลประเภทรันไทม์บางประเภท) และมีบางอย่างผิดพลาดและมีวัตถุจริงได้รับ มันเป็นจุดบกพร่อง เพียงอย่างเดียวควรจะบอกว่าสิ่งที่ไม่สามารถเกิดขึ้นได้
การเก็งกำไรที่บริสุทธิ์
แก้ไข:ดูเหมือนว่าฉันจะผิดในกรณีที่เป็นปัญหา OTOH IIRC บางภาษาอนุญาตให้เรียก vtbl จาก constructor destructor
ฉันใช้ VS2010 และเมื่อใดก็ตามที่ฉันพยายามโทรหา destructor โดยตรงจากวิธีสาธารณะฉันได้รับข้อผิดพลาด "การเรียกฟังก์ชันเสมือนจริง" ระหว่างรันไทม์
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void SomeMethod1() { this->~Foo(); }; /* ERROR */
};
ดังนั้นฉันจึงย้ายสิ่งที่อยู่ภายใน ~ Foo () เพื่อแยกวิธีส่วนตัวจากนั้นมันก็ใช้งานได้ดี
template <typename T>
class Foo {
public:
Foo<T>() {};
~Foo<T>() {};
public:
void _MethodThatDestructs() {};
void SomeMethod1() { this->_MethodThatDestructs(); }; /* OK */
};
หากคุณใช้ Borland / CodeGear / Embarcadero / Idera C ++ Builder คุณก็สามารถใช้งานได้
extern "C" void _RTLENTRY _pure_error_()
{
//_ErrorExit("Pure virtual function called");
throw Exception("Pure virtual function called");
}
ในขณะที่การดีบักจะวางเบรกพอยต์ในโค้ดและดู callstack ใน IDE มิฉะนั้นให้บันทึก call stack ในตัวจัดการข้อยกเว้นของคุณ (หรือฟังก์ชันนั้น) หากคุณมีเครื่องมือที่เหมาะสมสำหรับสิ่งนั้น ฉันเองใช้ MadExcept สำหรับสิ่งนั้น
ปล. การเรียกใช้ฟังก์ชันดั้งเดิมอยู่ใน [C ++ Builder] \ source \ cpprtl \ Source \ misc \ pureerr.cpp
นี่คือวิธีที่ส่อไปในทางที่จะเกิดขึ้น วันนี้ฉันเกิดขึ้นกับฉันเป็นหลัก
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
เห็นได้ชัดว่าไม่เป็นความจริงเพราะไม่ถูกต้อง: ฟังก์ชันเสมือนจริงจะถูกเรียกใช้เฉพาะเมื่อcallFoo()
ถูกเรียกภายในตัวสร้าง (หรือตัวทำลาย) เนื่องจากในขณะนี้วัตถุยังอยู่ (หรืออยู่แล้ว) ในระยะ A นี่คือโค้ดของคุณที่กำลังทำงานอยู่โดยไม่มีข้อผิดพลาดทางไวยากรณ์B b();
- วงเล็บทำให้เป็นการประกาศฟังก์ชันคุณต้องการอ็อบเจกต์