ฟังก์ชั่นเสมือนบริสุทธิ์พร้อมการติดตั้ง


176

ความเข้าใจพื้นฐานของฉันคือไม่มีการใช้งานสำหรับฟังก์ชั่นเสมือนจริง แต่ฉันก็บอกว่าอาจมีการใช้งานสำหรับฟังก์ชั่นเสมือนบริสุทธิ์

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

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

รหัสด้านบนตกลงหรือไม่

อะไรคือจุดประสงค์ในการทำให้มันเป็นฟังก์ชั่นเสมือนที่แท้จริงพร้อมการติดตั้ง

คำตอบ:


215

virtualฟังก์ชั่นที่บริสุทธิ์จะต้องดำเนินการในประเภทที่ได้รับที่จะเป็นอินสแตนซ์โดยตรง แต่ประเภทฐานยังสามารถกำหนดการดำเนินการ คลาสที่ได้รับสามารถเรียกการใช้คลาสพื้นฐานได้อย่างชัดเจน (หากสิทธิ์การอนุญาตอนุญาต) โดยใช้ชื่อที่มีการกำหนดขอบเขตเต็ม (โดยการโทรA::f()ในตัวอย่างของคุณ - ถ้าA::f()มีpublicหรือprotected) สิ่งที่ต้องการ:

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();
    }

};

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

โปรดทราบว่าแม้ว่ามันจะได้รับอนุญาตจากภาษา แต่ก็ไม่ใช่สิ่งที่ฉันเห็นโดยทั่วไปที่ใช้ (และความจริงที่ว่ามันสามารถทำได้ดูเหมือนจะแปลกใจที่โปรแกรมเมอร์ C ++ ส่วนใหญ่แม้แต่คนที่มีประสบการณ์)


1
คุณลืมที่จะเพิ่มว่าทำไมมันจึงทำให้โปรแกรมเมอร์ประหลาดใจ: เป็นเพราะข้อกำหนดแบบอินไลน์ถูกห้ามโดยมาตรฐาน deportedเพียวนิยามวิธีเสมือนต้อง (ใน. inl หรือ. cpp เพื่ออ้างถึงวิธีปฏิบัติในการตั้งชื่อไฟล์ทั่วไป)
v.oddou

ดังนั้นวิธีการโทรนี้เหมือนกับการเรียกวิธีสมาชิกแบบคงที่ วิธีการเรียนบางอย่างใน Java
Sany Liew

2
"ไม่ได้ใช้กันทั่วไป" == การปฏิบัติที่ไม่ดี? ฉันกำลังมองหาพฤติกรรมเดียวกันที่แน่นอนพยายามใช้ NVI และ NVI นั้นเป็นวิธีที่ดีสำหรับฉัน
Saskia

5
เป็นมูลค่าชี้ให้เห็นว่าการทำ A :: f () หมายถึงบริสุทธิ์ที่ B ต้องใช้ f () (มิฉะนั้น B จะเป็นนามธรรมและไม่มีความแน่นอน) และตามที่ @MichaelBurr ชี้ให้เห็นว่าการดำเนินการสำหรับ A :: f () หมายความว่า B อาจใช้เพื่อกำหนด f ()
fearless_fool

2
IIRC ชาวสกอตเมเยอร์มีบทความที่ยอดเยี่ยมเกี่ยวกับกรณีการใช้งานของคำถามนี้ในหนังสือคลาสสิกของเขาเรื่อง "C ++ ที่มีประสิทธิภาพมากขึ้น"
irsis

75

เพื่อความชัดเจนคุณเข้าใจผิดว่า = 0; หลังจากฟังก์ชั่นเสมือนหมายถึง

= 0 หมายถึงคลาสที่ได้รับจะต้องจัดให้มีการใช้งานไม่ว่าคลาสพื้นฐานจะไม่สามารถให้บริการได้

ในทางปฏิบัติเมื่อคุณทำเครื่องหมายฟังก์ชั่นเสมือนเป็น pure (= 0) มีจุดน้อยมากในการให้คำจำกัดความเพราะมันจะไม่ถูกเรียกเว้นแต่จะมีคนทำอย่างชัดเจนผ่าน Base :: Function (... ) หรือถ้า ตัวสร้างคลาสพื้นฐานเรียกใช้ฟังก์ชันเสมือนที่เป็นปัญหา


9
สิ่งนี้ไม่ถูกต้อง หากคุณเรียกใช้ฟังก์ชันเสมือนบริสุทธิ์ที่ตัวสร้างคลาสเสมือนจริงของคุณการเรียกเสมือนบริสุทธิ์จะเกิดขึ้น ในกรณีนี้คุณควรมีการนำไปใช้
rmn

@rmn ใช่คุณพูดถูกเกี่ยวกับการโทรเสมือนในตัวสร้าง ฉันอัพเดตคำตอบแล้ว หวังว่าทุกคนรู้ว่าจะไม่ทำเช่นนั้น :)
เทอร์รี่ Mahaffey

3
ในความเป็นจริงการโทรฐานบริสุทธิ์จากผลลัพธ์คอนสตรัคในพฤติกรรมที่กำหนดดำเนินการ ใน VC ++ นั่นจะทำให้ _purecall เกิดความผิดพลาด
Ofek Shilon

@OfekShilon ที่ถูกต้อง - ฉันจะถูกล่อลวงให้เรียกมันว่าพฤติกรรมที่ไม่ได้กำหนดและผู้สมัครสำหรับการปฏิบัติที่ไม่ดี / การปรับโครงสร้างรหัส (เช่นการเรียกวิธีการเสมือนภายในตัวสร้าง) ฉันเดาว่ามันเกี่ยวข้องกับ virtual table coherency ซึ่งอาจไม่พร้อมที่จะกำหนดเส้นทางไปยังส่วนของการใช้งานที่ถูกต้อง
teodron

1
ในตัวสร้างและ destructors ฟังก์ชันเสมือนไม่ใช่เสมือน
Jesper Juhl

20

ข้อดีของมันคือมันบังคับให้ประเภทที่ได้รับยังคงแทนที่วิธีการ แต่ยังให้การเริ่มต้นหรือการใช้งานเพิ่มเติม


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

19

หากคุณมีรหัสที่ควรดำเนินการโดยคลาสที่ได้รับมา แต่คุณไม่ต้องการให้เรียกใช้งานโดยตรง - และคุณต้องการบังคับให้รหัสนั้น overriden

รหัสของคุณถูกต้องแม้ว่าทั้งหมดนี้ไม่ได้เป็นคุณสมบัติที่ใช้บ่อยและมักจะเห็นเฉพาะเมื่อพยายามกำหนด destructor เสมือนแท้ - ในกรณีนั้นคุณต้องให้การใช้งาน สิ่งที่ตลกคือเมื่อคุณได้รับจากชั้นเรียนนั้นคุณไม่จำเป็นต้องแทนที่ผู้ทำลายล้าง

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

รหัสต่อไปนี้ถูกต้องอย่างน่าประหลาด:

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

Base::~Base() {}

class Derived : public Base {};

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

1
ตัวทำลายคลาสพื้นฐานจะถูกเรียกเสมอ, เสมือนหรือไม่และบริสุทธิ์หรือไม่; ด้วยฟังก์ชั่นอื่น ๆ คุณไม่สามารถรับประกันได้ว่าฟังก์ชั่นเสมือนที่เอาชนะจะเรียกใช้งานคลาสฐานไม่ว่าจะเป็นเวอร์ชันคลาสพื้นฐานหรือไม่ก็ตาม
CB Bailey

1
รหัสนั้นผิด คุณต้องกำหนด dtor นอกคำจำกัดความของคลาสเนื่องจากไวยากรณ์การเล่นโวหารของภาษา

@ Roger: ขอบคุณจริงๆที่ช่วยฉัน - นี่คือรหัสที่ฉันใช้มันรวบรวมได้ดีภายใต้ MSVC แต่ฉันคิดว่ามันจะไม่สามารถพกพาได้
Kornel Kisielewicz


4

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

Scott Meyers กล่าวถึงเรื่องนี้ในรายการC ++ (2nd Edition) ที่มีประสิทธิภาพ # 36 ความแตกต่างระหว่างการสืบทอดของอินเตอร์เฟสและการสืบทอดของการนำไปใช้งาน หมายเลขรายการอาจมีการเปลี่ยนแปลงในรุ่นล่าสุด


4

ฟังก์ชั่นเสมือนบริสุทธิ์ที่มีหรือไม่มีร่างกายก็หมายความว่าประเภทที่ได้รับจะต้องจัดให้มีการใช้งานของตนเอง

ฟังก์ชันของฟังก์ชันเสมือนบริสุทธิ์ในคลาสพื้นฐานนั้นมีประโยชน์ถ้าคลาสที่ได้รับของคุณต้องการเรียกใช้งานคลาสพื้นฐานของคุณ


2

'virtual void foo () = 0;' ไวยากรณ์ไม่ได้หมายความว่าคุณไม่สามารถใช้ foo () ในคลาสปัจจุบันได้ นอกจากนี้ยังไม่ได้หมายความว่าคุณต้องใช้มันในชั้นเรียนมา ก่อนที่คุณจะตบฉันลองสังเกตปัญหาเพชร: (โดยปริยายรหัสคุณ)

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;
}

ตอนนี้การเรียกใช้ obj-> foo () จะส่งผลให้ B :: foo () และ C :: bar ()

คุณเห็น ... วิธีการเสมือนจริงไม่ต้องนำไปใช้ในคลาสที่ได้รับ (foo () ไม่มีการใช้งานในคลาส C - คอมไพเลอร์จะรวบรวม) ใน C ++ มีช่องโหว่มากมาย

หวังว่าฉันจะช่วย :-)


5
ไม่จำเป็นต้องนำไปใช้ในคลาสที่ได้รับมาทั้งหมด แต่ต้องมีการนำไปใช้ในคลาสที่ได้รับทั้งหมดที่คุณต้องการสร้างอินสแตนซ์ คุณไม่สามารถสร้างวัตถุประเภทCในตัวอย่างของคุณได้ คุณสามารถยกตัวอย่างวัตถุของการพิมพ์Dเพราะได้รับการดำเนินการของจากfoo B
YoungJohn

0

กรณีใช้ที่สำคัญอย่างหนึ่งของการมีวิธีเสมือนจริงที่มีเนื้อความการนำไปใช้งานคือเมื่อคุณต้องการมีคลาสนามธรรม แต่คุณไม่มีวิธีการที่เหมาะสมในชั้นเรียนเพื่อทำให้บริสุทธิ์เสมือน ในกรณีนี้คุณสามารถสร้าง destructor ของ class pure virtual และนำการใช้งานที่คุณต้องการ (แม้แต่ร่างกายที่ว่างเปล่า) มาใช้ ตัวอย่างเช่น:

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

Foo::~Foo()
{
}

เทคนิคนี้ทำให้Fooคลาสเป็นนามธรรมและเป็นไปไม่ได้ที่จะยกตัวอย่างชั้นเรียนโดยตรง ในเวลาเดียวกันคุณยังไม่ได้เพิ่มวิธีเสมือนจริงเพิ่มเติมเพื่อให้Fooนามธรรมระดับ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.