ข้อขัดข้อง "การเรียกฟังก์ชันเสมือนจริง" มาจากไหน


107

บางครั้งฉันสังเกตเห็นโปรแกรมที่ขัดข้องในคอมพิวเตอร์ของฉันโดยมีข้อผิดพลาด: "pure virtual function call"

โปรแกรมเหล่านี้จะคอมไพล์ได้อย่างไรเมื่อวัตถุไม่สามารถสร้างคลาสนามธรรมได้

คำตอบ:


107

ซึ่งอาจเกิดขึ้นได้หากคุณพยายามเรียกใช้ฟังก์ชันเสมือนจากตัวสร้างหรือตัวทำลาย เนื่องจากคุณไม่สามารถเรียกใช้ฟังก์ชันเสมือนจากคอนสตรัคเตอร์หรือตัวทำลาย (อ็อบเจ็กต์คลาสที่ได้รับยังไม่ได้ถูกสร้างขึ้นหรือถูกทำลายไปแล้ว) จึงเรียกเวอร์ชันคลาสพื้นฐานซึ่งในกรณีของฟังก์ชันเสมือนจริงจะไม่ ไม่มีอยู่จริง

(ดูการสาธิตสดที่นี่ )

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
}

4
สาเหตุใดที่คอมไพเลอร์ไม่สามารถจับสิ่งนี้ได้โดยทั่วไป?
Thomas

21
ในกรณีทั่วไปไม่สามารถจับได้เนื่องจากการไหลจาก ctor สามารถไปได้ทุกที่และทุกที่สามารถเรียกใช้ฟังก์ชันเสมือนจริงได้ นี่คือการหยุดปัญหา 101.
shoosh

9
คำตอบผิดเล็กน้อย: ยังคงกำหนดฟังก์ชันเสมือนจริงได้โปรดดูรายละเอียดใน Wikipedia การใช้ถ้อยคำที่ถูกต้อง: อาจไม่มีอยู่
MSalters

5
ฉันคิดว่าตัวอย่างนี้ง่ายเกินไป: การdoIt()เรียกในตัวสร้างนั้นเบี่ยงเบนได้ง่ายและถูกส่งไปยังBase::doIt()แบบคงที่ซึ่งทำให้เกิดข้อผิดพลาดตัวเชื่อมโยง สิ่งที่เราต้องการจริงๆคือสถานการณ์ที่ประเภทไดนามิกในระหว่างการจัดส่งแบบไดนามิกคือประเภทฐานนามธรรม
Kerrek SB

2
สิ่งนี้สามารถทริกเกอร์ได้ด้วย MSVC หากคุณเพิ่มระดับพิเศษของทิศทาง: ให้Base::Baseเรียก non-virtual f()ซึ่งจะเรียกdoItวิธีการเสมือน (pure)
Frerich Raabe

64

เช่นเดียวกับกรณีมาตรฐานของการเรียกใช้ฟังก์ชันเสมือนจากตัวสร้างหรือตัวทำลายของวัตถุด้วยฟังก์ชันเสมือนจริงคุณยังสามารถเรียกใช้ฟังก์ชันเสมือนจริงได้ (บน 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 บางเวอร์ชัน)


1
ขอบคุณสำหรับตัวชี้เกี่ยวกับการเรียก _purecall () บนอินสแตนซ์ที่ถูกลบ ฉันไม่รู้เรื่องนี้ แต่เพิ่งพิสูจน์กับตัวเองด้วยรหัสทดสอบเล็กน้อย เมื่อดูการถ่ายโอนข้อมูลหลังการตายใน WinDbg ฉันคิดว่าฉันกำลังจัดการกับการแข่งขันที่เธรดอื่นพยายามใช้วัตถุที่ได้รับก่อนที่มันจะถูกสร้างขึ้นอย่างสมบูรณ์ แต่สิ่งนี้ทำให้เกิดแสงสว่างใหม่เกี่ยวกับปัญหาและดูเหมือนว่าจะเข้ากับหลักฐานได้ดีกว่า
Dave Ruske

1
อีกสิ่งหนึ่งที่ฉันจะเพิ่ม: การ_purecall()เรียกใช้ที่ปกติเกิดขึ้นในการเรียกเมธอดของอินสแตนซ์ที่ถูกลบจะไม่เกิดขึ้นหากคลาสพื้นฐานได้รับการประกาศด้วยการปรับให้__declspec(novtable)เหมาะสม (เฉพาะของ Microsoft) ด้วยเหตุนี้จึงเป็นไปได้ทั้งหมดที่จะเรียกวิธีการเสมือนจริงที่ถูกแทนที่หลังจากที่วัตถุถูกลบไปแล้วซึ่งอาจปกปิดปัญหาจนกว่ามันจะกัดคุณในรูปแบบอื่น _purecall()กับดักเป็นเพื่อนของคุณ!
Dave Ruske

เป็นประโยชน์ที่ได้รู้จัก Dave ฉันเคยเห็นสถานการณ์บางอย่างเมื่อเร็ว ๆ นี้ที่ฉันไม่ได้รับ purecalls เมื่อฉันคิดว่าฉันควรจะเป็น บางทีฉันอาจจะล้มเหลวจากการเพิ่มประสิทธิภาพนั้น
Len Holgate

1
@ เลนโฮลเกต: คำตอบที่มีค่ามาก นี่เป็นกรณีปัญหาของเราอย่างแน่นอน (การนับการอ้างอิงผิดที่เกิดจากเงื่อนไขการแข่งขัน) ขอบคุณมากที่ชี้ให้เราไปในทิศทางที่ถูกต้อง (เราสงสัยว่ามีการทุจริต v-table แทนและพยายามหารหัสผู้ร้ายอย่างบ้าคลั่ง)
BlueStrat

7

โดยปกติเมื่อคุณเรียกใช้ฟังก์ชันเสมือนผ่านตัวชี้ห้อยซึ่งส่วนใหญ่แล้วอินสแตนซ์จะถูกทำลายไปแล้ว

อาจมีเหตุผลที่ "สร้างสรรค์" มากขึ้นเช่นกัน: บางทีคุณอาจจัดการตัดส่วนของวัตถุที่ใช้ฟังก์ชันเสมือน แต่โดยปกติแล้วอินสแตนซ์นั้นถูกทำลายไปแล้ว


4

ฉันพบสถานการณ์ที่ฟังก์ชันเสมือนจริงถูกเรียกใช้เนื่องจากวัตถุที่ถูกทำลายLen Holgateมีคำตอบที่ดีมากอยู่แล้วฉันต้องการเพิ่มสีด้วยตัวอย่าง:

  1. วัตถุที่ได้รับจะถูกสร้างขึ้นและตัวชี้ (เป็นคลาสฐาน) จะถูกบันทึกไว้ที่ใดที่หนึ่ง
  2. วัตถุที่ได้รับจะถูกลบออกไป แต่ยังคงมีการอ้างอิงตัวชี้อยู่
  3. ตัวชี้ที่ชี้ไปยังวัตถุ Derived ที่ถูกลบจะถูกเรียก

ตัวทำลายคลาสที่ได้รับจะรีเซ็ตจุด 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เมื่อหน่วยความจำกลับสู่ระบบปฏิบัติการและโปรแกรมก็ไม่สามารถเข้าถึงได้ ดังนั้นสถานการณ์ "การเรียกใช้ฟังก์ชันเสมือนจริง" นี้มักจะเกิดขึ้นเมื่อวัตถุถูกจัดสรรบนพูลหน่วยความจำในขณะที่วัตถุถูกลบไปหน่วยความจำพื้นฐานจะไม่ถูกเรียกคืนโดยระบบปฏิบัติการ แต่กระบวนการนี้ยังสามารถเข้าถึงได้


0

ฉันเดาว่ามี vtbl ที่สร้างขึ้นสำหรับคลาสนามธรรมด้วยเหตุผลภายในบางอย่าง (อาจจำเป็นสำหรับข้อมูลประเภทรันไทม์บางประเภท) และมีบางอย่างผิดพลาดและมีวัตถุจริงได้รับ มันเป็นจุดบกพร่อง เพียงอย่างเดียวควรจะบอกว่าสิ่งที่ไม่สามารถเกิดขึ้นได้

การเก็งกำไรที่บริสุทธิ์

แก้ไข:ดูเหมือนว่าฉันจะผิดในกรณีที่เป็นปัญหา OTOH IIRC บางภาษาอนุญาตให้เรียก vtbl จาก constructor destructor


ไม่ใช่ข้อผิดพลาดในคอมไพเลอร์หากนั่นคือสิ่งที่คุณหมายถึง
Thomas

ความสงสัยของคุณถูกต้อง - C # และ Java อนุญาตสิ่งนี้ ในภาษาเหล่านั้น bohjects ที่กำลังก่อสร้างจะมีประเภทสุดท้าย ใน C ++ วัตถุจะเปลี่ยนประเภทระหว่างการก่อสร้างนั่นคือเหตุผลและเมื่อใดที่คุณสามารถมีวัตถุที่มีประเภทนามธรรมได้
MSalters

คลาสนามธรรมทั้งหมดและวัตถุจริงที่สร้างขึ้นจากพวกมันจำเป็นต้องมี vtbl (ตารางฟังก์ชันเสมือน) ซึ่งแสดงรายการฟังก์ชันเสมือนที่ควรเรียกใช้ ใน C ++ ออบเจ็กต์มีหน้าที่สร้างสมาชิกของตัวเองรวมถึงตารางฟังก์ชันเสมือน ตัวสร้างถูกเรียกจากคลาสพื้นฐานไปยังคลาสที่ได้รับและตัวทำลายถูกเรียกจากคลาสพื้นฐานที่ได้รับมาดังนั้นในคลาสพื้นฐานที่เป็นนามธรรมตารางฟังก์ชันเสมือนยังไม่พร้อมใช้งาน
fuzzyTew

0

ฉันใช้ 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 */
};

0

หากคุณใช้ 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


-2

นี่คือวิธีที่ส่อไปในทางที่จะเกิดขึ้น วันนี้ฉันเกิดขึ้นกับฉันเป็นหลัก

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
อย่างน้อยก็ไม่สามารถทำซ้ำใน vc2008 ของฉันได้ vptr จะชี้ไปที่ vtable ของ A เมื่อเริ่มต้นครั้งแรกในตัวควบคุมของ A แต่เมื่อ B ถูกเริ่มต้นอย่างสมบูรณ์ vptr จะเปลี่ยนเป็นชี้ไปที่ vtable ของ B ซึ่งก็โอเค
Baiyan Huang

coudnt ทำซ้ำด้วย vs2010 / 12
makc

I had this essentially happen to me todayเห็นได้ชัดว่าไม่เป็นความจริงเพราะไม่ถูกต้อง: ฟังก์ชันเสมือนจริงจะถูกเรียกใช้เฉพาะเมื่อcallFoo()ถูกเรียกภายในตัวสร้าง (หรือตัวทำลาย) เนื่องจากในขณะนี้วัตถุยังอยู่ (หรืออยู่แล้ว) ในระยะ A นี่คือโค้ดของคุณที่กำลังทำงานอยู่โดยไม่มีข้อผิดพลาดทางไวยากรณ์B b();- วงเล็บทำให้เป็นการประกาศฟังก์ชันคุณต้องการอ็อบเจกต์
Wolf
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.