คุณประกาศอินเตอร์เฟซใน C ++ ได้อย่างไร?


805

ฉันจะตั้งค่าคลาสที่แสดงถึงส่วนต่อประสานได้อย่างไร นี่เป็นเพียงระดับฐานนามธรรมหรือไม่


เป็นไปได้ซ้ำกับstackoverflow.com/questions/318064/…
null

คำตอบ:


686

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

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

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


106
Virtual desctuctor ++! สิ่งนี้สำคัญมาก คุณอาจต้องการรวมการประกาศเสมือนจริงของ operator = และคัดลอกคำจำกัดความของคอนสตรัคเตอร์เพื่อป้องกันการสร้างคอมไพเลอร์อัตโนมัติสำหรับคุณ
xan

33
อีกทางเลือกหนึ่งสำหรับ destructor เสมือนเป็น destructor ที่ได้รับการป้องกัน สิ่งนี้ปิดใช้งานการทำลายแบบ polymorphic ซึ่งอาจเหมาะสมกว่าในบางสถานการณ์ มองหา "แนวทาง # 4" ในgotw.ca/publications/mill18.htm
Fred Larson

9
อีกทางเลือกหนึ่งคือการกำหนด=0destructor เสมือนจริงบริสุทธิ์กับร่างกาย ข้อได้เปรียบตรงนี้คือคอมไพเลอร์สามารถตามหลักทฤษฎีดูว่า vtable ไม่มีสมาชิกที่ถูกต้องในตอนนี้และละทิ้งมันไปเลย ด้วย destructor เสมือนจริงที่มีร่างกายกล่าวว่า destructor สามารถเรียกได้ (จริง) เช่นในช่วงกลางของการก่อสร้างผ่านthisตัวชี้ (เมื่อวัตถุที่สร้างยังคงเป็นParentประเภท) และดังนั้นคอมไพเลอร์จะต้องให้ vtable ที่ถูกต้อง ดังนั้นหากคุณไม่ได้เรียก destructors เสมือนผ่านทางthisระหว่างการสร้าง :) คุณสามารถบันทึกขนาดรหัสได้
Pavel Minaev

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

18
อย่าลืมว่าใน C ++ 11 คุณสามารถระบุoverrideคำหลักเพื่ออนุญาตให้มีการรวบรวมอาร์กิวเมนต์เวลาและการตรวจสอบประเภทมูลค่า ตัวอย่างเช่นในการประกาศของเด็กvirtual void OverrideMe() override;
ฌอน

245

สร้างคลาสด้วยวิธีเสมือนบริสุทธิ์ ใช้อินเทอร์เฟซโดยการสร้างคลาสอื่นที่แทนที่วิธีเสมือนเหล่านั้น

วิธีเสมือนบริสุทธิ์เป็นวิธีการเรียนที่กำหนดเป็นเสมือนและได้รับมอบหมายให้เป็น 0

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

29
คุณควรมีตัวทำลายสิ่งใดสิ่งหนึ่งใน IDemo เพื่อให้มีการกำหนดพฤติกรรมที่ต้องทำ: IDemo * p = new Child; / * อะไรก็ตาม * / ลบ p;
Evan Teran

11
ทำไม OverrideMe ใน Child class ถึงเป็นเสมือนจริง? จำเป็นไหม?
Cemre

9
@ เซม - ไม่ไม่จำเป็น แต่ก็ไม่เจ็บเหมือนกัน
PowerApp101

11
โดยทั่วไปแล้วควรเก็บคำหลัก 'เสมือนจริง' ไว้ทุกครั้งที่มีการแทนที่วิธีเสมือน แม้ว่าจะไม่จำเป็น แต่ก็สามารถทำให้รหัสชัดเจนขึ้น - ไม่เช่นนั้นคุณไม่มีข้อบ่งชี้ว่าวิธีการนั้นสามารถใช้ polymorphically หรือแม้แต่มีอยู่ในคลาสพื้นฐาน
Kevin

27
@Kevin ยกเว้นoverrideใน C ++ 11
keyser

146

เหตุผลทั้งหมดที่คุณมีประเภทอินเตอร์เฟสพิเศษนอกเหนือจากคลาสพื้นฐานแบบนามธรรมใน C # / Javaเป็นเพราะ C # / Java ไม่สนับสนุนการสืบทอดหลายรายการ

C ++ รองรับการสืบทอดหลายแบบดังนั้นจึงไม่จำเป็นต้องใช้ชนิดพิเศษ คลาสพื้นฐานแบบ abstract ที่ไม่มีเมธอด (virtual virtual) ที่ไม่ใช่แบบนามธรรมจะเทียบเท่ากับอินเตอร์เฟส C # / Java


17
มันจะเป็นการดีที่จะสามารถสร้างส่วนต่อประสานเพื่อช่วยให้เราพิมพ์ได้ไม่มาก (virtual, = 0, destructor เสมือน) และฉันก็ไม่เคยเห็นมันมาใช้ในทางปฏิบัติ แต่จำเป็นต้องมีการเชื่อมต่อตลอดเวลา หากต้องการลบ C ++ comity จะไม่แนะนำอินเทอร์เฟซเพียงเพราะฉันต้องการ
Ha11owed

9
Ha11owed: มันมีส่วนต่อประสาน พวกมันถูกเรียกว่าคลาสที่มีเมธอดเสมือนจริงและไม่มีการใช้เมธอด
เส้นทาง Miles Rout

6
@doc: java.lang.Thread มีเมธอดและค่าคงที่ที่คุณอาจไม่ต้องการในวัตถุของคุณ คอมไพเลอร์ควรทำอย่างไรถ้าคุณขยายจากเธรดและคลาสอื่นด้วยเมธอด public checkAccess () คุณต้องการใช้พอยน์เตอร์พอยน์เตอร์ที่มีชื่ออย่างยิ่งเช่นใน C ++ หรือไม่? ดูเหมือนว่าการออกแบบที่ไม่ดีคุณมักจะต้องการองค์ประกอบที่คุณคิดว่าคุณต้องได้รับมรดกหลายอย่าง
Ha11owed

4
@ Ha11owed เมื่อนานมาแล้วดังนั้นฉันไม่จำรายละเอียดได้ แต่มีวิธีและเนื้อหาที่ฉันต้องการมีในชั้นเรียนและที่สำคัญกว่านั้นฉันต้องการให้วัตถุคลาสที่ได้รับมาเป็นThreadตัวอย่าง การสืบทอดหลายอย่างอาจเป็นการออกแบบที่ไม่ดีเช่นเดียวกับองค์ประกอบ ทุกอย่างขึ้นอยู่กับกรณี
doc

2
@Dave: จริงเหรอ? วัตถุประสงค์ -C มีการประเมินเวลารวบรวมและแม่แบบ?
Deduplicator

51

ไม่มีแนวคิด "อินเตอร์เฟส" ต่อ se ใน C ++ AFAIK มีการใช้งานอินเตอร์เฟสเป็นครั้งแรกใน Java เพื่อหลีกเลี่ยงการขาดการสืบทอดหลายอย่าง แนวคิดนี้มีประโยชน์มากและสามารถใช้เอฟเฟกต์เดียวกันใน C ++ ได้โดยใช้คลาสฐานนามธรรม

คลาสฐานนามธรรมเป็นคลาสที่ฟังก์ชันสมาชิกอย่างน้อยหนึ่งฟังก์ชัน (เมธอดใน Java lingo) เป็นฟังก์ชันเสมือนจริงที่ประกาศโดยใช้ไวยากรณ์ต่อไปนี้:

class A
{
  virtual void foo() = 0;
};

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

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

และดังที่มาร์คแรนซัมชี้ให้เห็นคลาสฐานนามธรรมควรจัดเตรียม destructor เสมือนเช่นเดียวกับคลาสพื้นฐานใด ๆ สำหรับเรื่องนั้น


13
มากกว่า "การขาดมรดกหลายอย่าง" ฉันจะพูดเพื่อแทนที่มรดกหลายอย่าง Java ได้รับการออกแบบเช่นนี้ตั้งแต่เริ่มต้นเนื่องจากการสืบทอดหลายรายการสร้างปัญหามากกว่าที่จะแก้ไข คำตอบที่ดี
OscarRyz

11
ออสการ์นั้นขึ้นอยู่กับว่าคุณเป็นโปรแกรมเมอร์ C ++ ผู้เรียนรู้ Java หรือในทางกลับกัน :) IMHO ถ้าใช้อย่างมีเหตุผลเช่นเดียวกับเกือบทุกอย่างใน C ++ การสืบทอดหลายอย่างจะช่วยแก้ปัญหา คลาสฐานนามธรรม "อินเตอร์เฟส" เป็นตัวอย่างของการใช้การสืบทอดหลายครั้งอย่างรอบคอบ
Dima

8
@OscarRyz ผิด MI เท่านั้นสร้างปัญหาเมื่อนำไปใช้ในทางที่ผิด ปัญหาที่ถูกกล่าวหามากที่สุดเกี่ยวกับ MI ก็จะเกิดขึ้นกับการออกแบบทางเลือก (ไม่มี MI) เมื่อผู้คนมีปัญหากับการออกแบบกับ MI มันเป็นความผิดของ MI; หากพวกเขามีปัญหาการออกแบบกับ SI มันเป็นความผิดของตัวเอง "เพชรแห่งความตาย" (มรดกซ้ำ) เป็นตัวอย่างสำคัญ การทุบตี MI ไม่ใช่ความเจ้าเล่ห์ที่บริสุทธิ์ แต่ใกล้เคียง
curiousguy

4
อินเทอร์เฟซต่างจากคลาสนามธรรมดังนั้นอินเทอร์เฟซของ Java ไม่ได้เป็นเพียงแค่วิธีแก้ปัญหาด้านเทคนิค ตัวเลือกระหว่างการกำหนดอินเทอร์เฟซหรือคลาสนามธรรมถูกขับเคลื่อนโดยความหมายไม่ใช่ข้อพิจารณาทางเทคนิค ลองนึกภาพบางส่วนของอินเทอร์เฟซ "HasEngine": นั่นคือแง่มุมคุณลักษณะและสามารถนำไปใช้ / นำไปใช้โดยประเภทที่แตกต่างกันมาก (ไม่ว่าจะเป็นคลาสหรือคลาสนามธรรม) ดังนั้นเราจะกำหนดอินเทอร์เฟซให้
Marek Stanley

2
@MarekStanley คุณอาจพูดถูก แต่ฉันหวังว่าคุณจะได้รับตัวอย่างที่ดีกว่า ฉันชอบคิดในแง่ของการสืบทอดส่วนต่อประสานกับการสืบทอดการใช้งาน ใน C ++ คุณสามารถรับทั้งอินเทอร์เฟซและการใช้งานร่วมกัน (การสืบทอดสาธารณะ) หรือคุณสามารถสืบทอดการปรับใช้เท่านั้น ใน Java คุณมีตัวเลือกในการรับมรดกเพียงส่วนต่อประสานโดยไม่มีการใช้งาน
Dima

43

เท่าที่ฉันสามารถทดสอบได้สิ่งสำคัญคือการเพิ่มตัวทำลายเสมือน ฉันใช้วัตถุที่สร้างขึ้นnewและทำลายด้วยdeleteและถูกทำลายด้วย

ถ้าคุณไม่เพิ่ม destructor เสมือนในอินเทอร์เฟซแล้ว destructor ของคลาสที่สืบทอดมาจะไม่ถูกเรียก

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

หากคุณเรียกใช้รหัสก่อนหน้าโดยไม่มีvirtual ~IBase() {};คุณจะเห็นว่า destructor Tester::~Tester()ไม่เคยถูกเรียก


3
คำตอบที่ดีที่สุดในหน้านี้เนื่องจากทำให้เป็นจุดโดยให้ตัวอย่างที่เป็นประโยชน์และรวบรวมได้ ไชโย!
Lumi

1
Testet :: ~ Tester () ทำงานเฉพาะเมื่อ obj คือ "Declared with Tester"
Alessandro L.

ที่จริงแล้วตัวทำลายสตริงส่วนบุคคลจะถูกเรียกใช้และในหน่วยความจำนั่นคือทั้งหมดที่จะถูกจัดสรรให้ เท่าที่รันไทม์เกี่ยวข้องเมื่อสมาชิกคอนกรีตทั้งหมดของคลาสถูกทำลายดังนั้นอินสแตนซ์ของคลาสคือ ฉันลองการทดลองที่คล้ายกันกับคลาส Line ที่มีจุดโครงสร้างสองจุดและพบว่าโครงสร้างทั้งสองถูกทำลาย (Ha!) เมื่อมีการลบหรือเรียกคืนจากฟังก์ชันที่ครอบคลุม valgrind ยืนยันการรั่วไหล 0 ครั้ง
Chris Reid

33

คำตอบของฉันนั้นเหมือนกับคนอื่น ๆ แต่ฉันคิดว่ามีอีกสองสิ่งที่สำคัญที่ต้องทำ:

  1. ประกาศ destructor IDemoเสมือนในอินเตอร์เฟซของคุณหรือให้ได้รับการป้องกันที่ไม่เสมือนหนึ่งเพื่อหลีกเลี่ยงพฤติกรรมที่ไม่ได้กำหนดถ้ามีคนพยายามที่จะลบวัตถุของการพิมพ์

  2. ใช้การสืบทอดเสมือนเพื่อหลีกเลี่ยงปัญหาที่มีหลายการสืบทอด (บ่อยครั้งที่มีการสืบทอดหลายอย่างเมื่อเราใช้ส่วนต่อประสาน)

และเช่นเดียวกับคำตอบอื่น ๆ :

  • สร้างคลาสด้วยวิธีเสมือนบริสุทธิ์
  • ใช้อินเทอร์เฟซโดยการสร้างคลาสอื่นที่แทนที่วิธีเสมือนเหล่านั้น

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }

    หรือ

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }

    และ

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }

2
ไม่จำเป็นต้องใช้การสืบทอดเสมือนเนื่องจากคุณไม่มีสมาชิกข้อมูลใด ๆ ในส่วนต่อประสาน
Robocide

3
การสืบทอดเสมือนมีความสำคัญสำหรับวิธีการเช่นกัน หากไม่มีมันคุณจะพบกับความคลุมเครือด้วย OverrideMe () แม้ว่าหนึ่งใน 'อินสแตนซ์' ของมันเป็นเสมือนจริง
Knarf Navillus

5
@Avishay_ " ไม่จำเป็นต้องมีการสืบทอดเสมือนเนื่องจากคุณไม่มีสมาชิกข้อมูลใด ๆ ในอินเทอร์เฟซ " ผิด
curiousguy

โปรดสังเกตว่าการสืบทอดเสมือนอาจไม่ทำงานในบางรุ่น gcc เป็นรุ่น 4.3.3 ซึ่งมาพร้อมกับ WinAVR 2010: gcc.gnu.org/bugzilla/show_bug.cgi?id=35067
mMontu

-1 สำหรับการมี destructor ที่ไม่มีการป้องกันเสมือนขออภัย
Wolf

10

ใน C ++ 11 คุณสามารถหลีกเลี่ยงการสืบทอดอย่างง่ายดาย:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

ในกรณีนี้อินเทอร์เฟซที่มีการอ้างอิงความหมายคือคุณต้องตรวจสอบให้แน่ใจว่าวัตถุที่อยู่เหนืออินเทอร์เฟซ

อินเตอร์เฟสประเภทนี้มีข้อดีข้อเสีย:

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

บอกว่าฉันมีแอปพลิเคชันที่ฉันจัดการกับรูปร่างของฉัน polymorphically โดยใช้MyShapeอินเทอร์เฟซ:

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

ในแอปพลิเคชันของคุณคุณทำสิ่งเดียวกันกับรูปร่างที่แตกต่างกันโดยใช้YourShapeอินเตอร์เฟส:

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

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

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

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

อัปเดต: มีการอ้างอิงใหม่สองสามรายการเกี่ยวกับความหลากหลายที่มีลักษณะไม่สืบทอด:


5
การสืบทอด TBH นั้นชัดเจนยิ่งกว่าสิ่ง C ++ 11 ซึ่งแสร้งทำเป็นอินเทอร์เฟซ แต่เป็นกาวสำหรับผูกการออกแบบที่ไม่สอดคล้องกันบางอย่าง ตัวอย่างรูปร่างแยกออกจากความเป็นจริงและCircleคลาสเป็นงานออกแบบที่ไม่ดี คุณควรใช้Adapterรูปแบบในกรณีเช่นนี้ ขออภัยถ้ามันฟังดูรุนแรงนิดหน่อย แต่ลองใช้ห้องสมุดชีวิตจริงบางอย่างQtก่อนตัดสินใจเกี่ยวกับการรับมรดก มรดกทำให้ชีวิตง่ายขึ้นมาก
doc

2
มันไม่ได้ฟังดูรุนแรงเลย ตัวอย่างรูปร่างหลุดจากความเป็นจริงเป็นอย่างไร? คุณช่วยยกตัวอย่างการแก้ไข Circle โดยใช้Adapterรูปแบบได้ไหม ฉันสนใจที่จะดูข้อดีของมัน
gnzlbg

ตกลงฉันจะพยายามใส่ในกล่องเล็ก ๆ นี้ ก่อนอื่นคุณมักจะเลือกไลบรารี่เช่น "MyShape" ก่อนที่จะเริ่มเขียนใบสมัครของคุณเองเพื่อความปลอดภัยในการทำงาน มิฉะนั้นคุณจะรู้ได้อย่างไรว่าSquareยังไม่ได้อยู่ที่นั่น? ญาณ? นั่นเป็นเหตุผลว่าทำไมมันถึงแยกออกมาจากความเป็นจริง และในความเป็นจริงหากคุณเลือกที่จะใช้ไลบรารี่ "MyShape" คุณสามารถปรับใช้อินเทอร์เฟซของมันได้ตั้งแต่เริ่มต้น ในตัวอย่างรูปร่างมีเรื่องไร้สาระมากมาย (หนึ่งในนั้นคือคุณมีสองCircleโครงสร้าง) แต่อะแดปเตอร์จะมีลักษณะเช่นนั้น -> ideone.com/UogjWk
doc

2
มันไม่ได้แยกออกจากความเป็นจริงแล้ว เมื่อ บริษัท A ซื้อ บริษัท B และต้องการรวม codebase ของ บริษัท B เข้ากับ A คุณจะมีสองฐานรหัสที่เป็นอิสระอย่างสมบูรณ์ ลองนึกภาพว่าแต่ละคนมีลำดับชั้นของรูปร่างที่แตกต่างกัน คุณไม่สามารถรวมมันเข้ากับการสืบทอดได้อย่างง่ายดายและเพิ่ม บริษัท C และคุณมีระเบียบมาก ฉันคิดว่าคุณควรจะดูการสนทนานี้: youtube.com/watch?v=0I0FD3N5cgMคำตอบของฉันเก่ากว่า แต่คุณจะเห็นความคล้ายคลึงกัน คุณไม่จำเป็นต้องปรับใช้ทุกอย่างตลอดเวลาคุณสามารถจัดทำการนำไปใช้ในส่วนต่อประสานและเลือกฟังก์ชั่นสมาชิกหากมี
gnzlbg

1
ฉันดูวิดีโอแล้วและนี่ก็ผิดทั้งหมด ฉันไม่เคยใช้ dynamic_cast ยกเว้นเพื่อจุดประสงค์ในการดีบั๊ก การส่งไดนามิกหมายถึงมีบางอย่างผิดปกติกับการออกแบบและการออกแบบของคุณในวิดีโอนี้ผิดไปจากการออกแบบ :) ผู้ชายถึงกับพูดถึง Qt แต่ถึงอย่างนี้เขาก็ผิด - QLayout ไม่ได้รับมรดกจาก QWidget หรืออย่างอื่น!
doc

9

คำตอบที่ดีทั้งหมดข้างต้น สิ่งหนึ่งที่พิเศษที่คุณควรจำไว้ - คุณสามารถมี destructor เสมือนแท้ ข้อแตกต่างเพียงอย่างเดียวคือคุณยังต้องใช้งาน

สับสน?


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

เหตุผลหลักที่คุณต้องการทำคือถ้าคุณต้องการให้วิธีการอินเทอร์เฟซอย่างที่ฉันมี แต่ให้เอาชนะพวกเขาไม่จำเป็น

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

การปรับใช้ destructor ใหม่ในคลาสที่ได้รับไม่ใช่เรื่องใหญ่เลย - ฉันมักจะปรับใช้ destructor เสมือนหรือไม่ในคลาสที่ได้รับของฉันเสมอ


4
ทำไมโอ้ทำไมถึงมีคนอยากทำ dtor ในกรณีนี้ให้เป็นเสมือนจริง? สิ่งที่จะได้รับจากสิ่งนั้นคืออะไร? คุณแค่บังคับบางสิ่งให้กับคลาสที่ได้รับซึ่งพวกเขาอาจไม่จำเป็นต้องรวม - dtor
Johann Gerell

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

7

หากคุณใช้คอมไพเลอร์ C ++ ของ Microsoft คุณสามารถทำสิ่งต่อไปนี้:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

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


4
ฉันไม่ค่อยเห็นว่าทำไมคุณถึงใช้novtableเกินมาตรฐานvirtual void Bar() = 0;
Flexo

2
นอกจากนี้ (ฉันเพิ่งสังเกตเห็นสิ่งที่ขาด= 0;ซึ่งฉันเพิ่มไว้) อ่านเอกสารหากคุณไม่เข้าใจ
ทำเครื่องหมายอินแกรม

ฉันอ่านมันโดยไม่ใช้= 0;และคิดว่ามันเป็นวิธีที่ไม่ได้มาตรฐานในการทำสิ่งเดียวกัน
เฟล็กโซ

4

นอกเหนือจากสิ่งที่เขียนอยู่ที่นั่น:

ขั้นแรกให้แน่ใจว่า destructor ของคุณเป็นเวอร์ช่วลเสมือนจริง

ประการที่สองคุณอาจต้องการรับมรดกอย่างแท้จริง (มากกว่าปกติ) เมื่อคุณดำเนินการเพียงเพื่อมาตรการที่ดี


ฉันชอบการสืบทอดเสมือนจริงเนื่องจากในเชิงแนวคิดมันหมายความว่ามีเพียงอินสแตนซ์เดียวของคลาสที่สืบทอด เป็นที่ยอมรับคลาสที่นี่ไม่มีความต้องการพื้นที่ใด ๆ ดังนั้นจึงอาจไม่จำเป็น ฉันยังไม่เคยทำ MI ใน C ++ มาสักพักหนึ่งแล้ว แต่การรับมรดกที่ไม่ใช่เสมือนจะไม่ทำให้การถ่ายทอดสดยุ่งยากหรือไม่
Uri

ทำไมโอ้ทำไมถึงมีคนอยากทำ dtor ในกรณีนี้ให้เป็นเสมือนจริง? สิ่งที่จะได้รับจากสิ่งนั้นคืออะไร? คุณแค่บังคับบางสิ่งให้กับคลาสที่ได้รับซึ่งพวกเขาอาจไม่จำเป็นต้องรวม - dtor
Johann Gerell

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

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

+1 สำหรับการสืบทอดเสมือนจริงเนื่องจากมีส่วนต่อประสานที่มีแนวโน้มว่าคลาสจะได้รับส่วนต่อประสานจากสองเส้นทางขึ้นไป ฉันเลือกใช้ destructors ที่ได้รับการปกป้องในอินเตอร์เฟส
doc

4

คุณยังสามารถพิจารณาคลาสสัญญาที่ใช้กับ NVI (รูปแบบอินเตอร์เฟสเสมือนจริง) ตัวอย่างเช่น

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1();
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    virtual void do_f(Parameters p); // From contract 1.
    virtual void do_g(Parameters p); // From contract 2.
};

สำหรับผู้อ่านคนอื่นบทความของ Dr Dobbs เรื่อง "การสนทนา: ความจริงของคุณ" โดย Jim Hyslop และ Herb Sutter อธิบายเพิ่มเติมอีกเล็กน้อยเกี่ยวกับสาเหตุที่คน ๆ หนึ่งอาจต้องการใช้ NVI
user2067021

และบทความนี้ "เสมือนจริง" โดย Herb Sutter
user2067021

1

ฉันยังใหม่ในการพัฒนา C ++ ฉันเริ่มต้นด้วย Visual Studio (VS)

แต่ดูเหมือนจะไม่มีใครกล่าวถึง__interfaceใน VS (.NET) ฉันไม่แน่ใจว่านี่เป็นวิธีที่ดีในการประกาศอินเทอร์เฟซ แต่ดูเหมือนว่าจะมีการบังคับใช้เพิ่มเติม (กล่าวถึงในเอกสาร ) เช่นที่คุณไม่ต้องระบุอย่างชัดเจนvirtual TYPE Method() = 0;เนื่องจากมันจะถูกแปลงโดยอัตโนมัติ

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};

อย่างไรก็ตามฉันไม่ได้ใช้เพราะฉันกังวลเกี่ยวกับความเข้ากันได้ของการรวบรวมข้ามแพลตฟอร์มเนื่องจากมีเฉพาะใน. NET

หากใครมีสิ่งที่น่าสนใจเกี่ยวกับเรื่องนี้โปรดแบ่งปัน :-)

ขอบคุณ


0

ในขณะที่มันเป็นความจริงที่virtualเป็นมาตรฐาน de-facto เพื่อกำหนดอินเทอร์เฟซ แต่อย่าลืมเกี่ยวกับรูปแบบ C-like แบบคลาสสิกซึ่งมาพร้อมกับ Constructor ใน C ++:

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

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

เคล็ดลับ:

  • คุณอาจรับช่วงต่อจากนี้เป็นคลาสพื้นฐาน (อนุญาตทั้งแบบเสมือนจริงและไม่ใช่เสมือน) และเติม clickของลูกหลานของคุณ
  • คุณอาจมีตัวชี้ฟังก์ชั่นเป็นprotectedสมาชิกและมีpublicอ้างอิงและ / หรือทะเยอทะยาน
  • ดังกล่าวข้างต้นนี้ช่วยให้คุณเปลี่ยนการใช้งานในรันไทม์ ดังนั้นจึงเป็นวิธีการจัดการสถานะเช่นกัน ขึ้นอยู่กับจำนวนของifการเปลี่ยนแปลงสถานะเมื่อเทียบกับรหัสของคุณนี่อาจเร็วกว่าswitch()es หรือifs (คาดว่าจะมีการพลิกกลับประมาณ 3-4 ifวินาที แต่ควรทำการวัดก่อนเสมอ
  • หากคุณเลือกที่std::function<>มากกว่าคำแนะนำการทำงานคุณอาจIBaseจะสามารถจัดการข้อมูลของวัตถุทั้งหมดของคุณภายใน จากจุดนี้คุณสามารถมีแผนงานสำหรับIBase(เช่นstd::vector<IBase>จะทำงาน) โปรดทราบว่าสิ่งนี้อาจช้ากว่านี้ขึ้นอยู่กับคอมไพเลอร์และรหัส STL ของคุณ การใช้งานในปัจจุบันของstd::function<>มักจะมีค่าใช้จ่ายเมื่อเทียบกับตัวชี้ฟังก์ชั่นหรือแม้กระทั่งฟังก์ชั่นเสมือน (อาจมีการเปลี่ยนแปลงในอนาคต)

0

นี่คือคำจำกัดความของabstract classมาตรฐาน c ++

n4687

13.4.2

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


-2
class Shape 
{
public:
   // pure virtual function providing interface framework.
   virtual int getArea() = 0;
   void setWidth(int w)
   {
      width = w;
   }
   void setHeight(int h)
   {
      height = h;
   }
protected:
    int width;
    int height;
};

class Rectangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height); 
    }
};
class Triangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height)/2; 
    }
};

int main(void)
{
     Rectangle Rect;
     Triangle  Tri;

     Rect.setWidth(5);
     Rect.setHeight(7);

     cout << "Rectangle area: " << Rect.getArea() << endl;

     Tri.setWidth(5);
     Tri.setHeight(7);

     cout << "Triangle area: " << Tri.getArea() << endl; 

     return 0;
}

ผลลัพธ์: พื้นที่สี่เหลี่ยมผืนผ้า: 35 พื้นที่สามเหลี่ยม: 17

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


5
นี่ไม่ใช่สิ่งที่ถือว่าเป็นส่วนต่อประสาน! นั่นเป็นเพียงระดับฐานนามธรรมด้วยวิธีการหนึ่งที่จะต้องถูกแทนที่! โดยทั่วไปแล้วอินเตอร์เฟสจะเป็นวัตถุที่มีข้อกำหนดของเมธอดเท่านั้น - คลาสอื่น ๆ "สัญญา" ต้องทำตามเมื่อใช้อินเทอร์เฟซ
guitarflow
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.