นัยกับอินเทอร์เฟซชัดเจน


9

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

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

ความคิดใด ๆ เกี่ยวกับเรื่องนี้?

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

บางโพสต์ที่เกี่ยวข้อง:


นี่คือตัวอย่างที่จะทำให้คำถามนี้เป็นรูปธรรมมากขึ้น:

การเชื่อมต่อโดยนัย:

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

อินเตอร์เฟซที่ชัดเจน:

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

ตัวอย่างที่เป็นรูปธรรมในเชิงลึกยิ่งขึ้น:

ปัญหา C ++ บางอย่างสามารถแก้ไขได้ด้วย:

  1. คลาส templated ที่มีประเภทเทมเพลตจัดเตรียมอินเตอร์เฟส
  2. คลาสที่ไม่ใช่เท็มเพลตที่ใช้ตัวชี้คลาสเบสซึ่งจัดเตรียมอินเตอร์เฟสที่ชัดเจน

รหัสที่ไม่เปลี่ยนแปลง:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

กรณีที่ 1 คลาสที่ไม่ใช่เท็มเพลตที่ใช้ตัวชี้คลาสเบสซึ่งจัดเตรียมอินเตอร์เฟสที่ชัดเจน:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

กรณีที่ 2 คลาสที่มีเทมเพลตซึ่งมีประเภทเทมเพลตจัดเตรียมอินเตอร์เฟสโดยนัย:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

กรณีที่ 3 คลาสที่มีเทมเพลตซึ่งมีประเภทเทมเพลตจัดเตรียมอินเทอร์เฟซโดยนัย (ในเวลานี้ไม่ใช่ที่มาจากCoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

กรณีที่ 1 ต้องการให้วัตถุที่ถูกส่งผ่านเข้าuseCoolClass()มาเป็นลูกของCoolClass(และนำไปใช้worthless()) ในกรณีที่ 2 และ 3 จะใช้คลาสใดก็ได้ที่มีdoSomethingCool()ฟังก์ชั่น

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


บางทีฉันอาจจะขาดอะไรบางอย่างไป แต่ความแตกต่างที่สำคัญนั้นไม่ได้ระบุไว้อย่างชัดเจนในย่อหน้าแรกของคุณซึ่งก็คืออินเทอร์เฟซที่ชัดเจนคือ polymorphism แบบรันไทม์ในขณะที่อินเตอร์เฟสโดยนัยนั้น
Robert Harvey

2
มีปัญหาบางอย่างที่สามารถแก้ไขได้โดยการมีคลาสหรือฟังก์ชันที่ใช้ตัวชี้ไปยังคลาสนามธรรม (ซึ่งมีอินเทอร์เฟซที่ชัดเจน) หรือโดยการมีคลาสหรือฟังก์ชัน templated ที่ใช้วัตถุที่มีอินเทอร์เฟซโดยนัย โซลูชันทั้งสองทำงาน คุณต้องการใช้โซลูชันแรกเมื่อใด ที่สอง?
Chris Morris

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

คำตอบ:


8

คุณได้กำหนดไว้แล้วที่สำคัญจุดหนึ่งคือเวลาทำงานและอื่น ๆ ที่รวบรวมเวลา ข้อมูลจริงที่คุณต้องการคือการกระจายของตัวเลือกนี้

Compiletime:

  • Pro: อินเทอร์เฟซการคอมไพล์เวลาเป็น granular มากกว่าที่เวลาทำงาน จากนั้นสิ่งที่ฉันหมายถึงคือคุณสามารถใช้เพียงความต้องการของฟังก์ชั่นเดียวหรือชุดของฟังก์ชั่นในขณะที่คุณเรียกพวกเขา คุณไม่จำเป็นต้องทำอินเทอร์เฟซทั้งหมด ข้อกำหนดเป็นเพียงสิ่งที่คุณต้องการเท่านั้น
  • Pro: เทคนิคอย่าง CRTP หมายความว่าคุณสามารถใช้อินเทอร์เฟซแบบปริยายเพื่อการใช้งานเริ่มต้นของสิ่งต่าง ๆ เช่นตัวดำเนินการ คุณไม่สามารถทำสิ่งนี้ได้ด้วยการสืบทอดมรดก
  • Pro: อินเทอร์เฟซโดยปริยายนั้นง่ายกว่ามากในการเขียนและทวีคูณ "สืบทอด" มากกว่าอินเตอร์เฟสแบบรันไทม์และไม่กำหนดข้อ จำกัด ไบนารีใด ๆ - ตัวอย่างเช่นคลาส POD สามารถใช้อินเทอร์เฟซโดยนัย ไม่จำเป็นต้องมีการvirtualสืบทอดหรือเสิ่นนิแกนอื่น ๆ ที่มีอินเตอร์เฟสโดยนัยซึ่งเป็นข้อได้เปรียบที่ยิ่งใหญ่
  • Pro: คอมไพเลอร์สามารถทำวิธีการเพิ่มประสิทธิภาพมากขึ้นสำหรับอินเทอร์เฟซเวลารวบรวม นอกจากนี้ความปลอดภัยประเภทพิเศษทำให้รหัสปลอดภัยยิ่งขึ้น
  • Pro: เป็นไปไม่ได้ที่จะทำการพิมพ์ค่าสำหรับอินเทอร์เฟซที่ทำงานเนื่องจากคุณไม่ทราบขนาดหรือการจัดตำแหน่งของวัตถุสุดท้าย ซึ่งหมายความว่ากรณีใด ๆ ที่ต้องการ / ผลประโยชน์จากการพิมพ์ค่าจะได้รับประโยชน์มากจากเทมเพลต
  • คอนดิชั่น: เทมเพลตเป็นตัวเมียในการคอมไพล์และใช้งาน, และพวกมันสามารถทำการเล่นซอระหว่างคอมไพเลอร์
  • คอนดิชั่น: ไม่สามารถโหลดเทมเพลตขณะใช้งาน (ชัด) ดังนั้นจึงมีข้อ จำกัด ในการแสดงโครงสร้างข้อมูลแบบไดนามิกตัวอย่างเช่น

Runtime:

  • Pro: ไม่ต้องตัดสินใจเลือกประเภทสุดท้ายจนกว่าจะถึงเวลาใช้งาน ซึ่งหมายความว่าการสืบทอดขณะใช้งานสามารถแสดงโครงสร้างข้อมูลบางอย่างได้ง่ายขึ้นมากถ้าแม่แบบสามารถทำได้ นอกจากนี้คุณยังสามารถส่งออกชนิด polymorphic แบบรันไทม์ข้ามขอบเขต C เช่น COM
  • Pro: มันง่ายกว่ามากในการระบุและใช้การสืบทอดแบบรันไทม์และคุณจะไม่ได้รับลักษณะการทำงานเฉพาะของคอมไพเลอร์
  • คอนดิชั่น: การสืบทอดเวลาทำงานอาจช้ากว่าคอมไพล์เวลาคอมไพล์
  • Con: การสืบทอดเวลาทำงานสูญเสียข้อมูลชนิด
  • คอนดิชั่น: การสืบทอดเวลาทำงานมีความยืดหยุ่นน้อยกว่า
  • คอนดิชั่น: หลายมรดกเป็นผู้หญิงเลว

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

แก้ไข: มันน่าสังเกตว่าใน C ++ โดยเฉพาะอย่างยิ่งมีการใช้สำหรับการสืบทอดอื่นที่ไม่ใช่ polymorphism แบบรันไทม์ ตัวอย่างเช่นคุณสามารถสืบทอด typedefs หรือใช้สำหรับการติดแท็กประเภทหรือใช้ CRTP แม้ว่าในท้ายที่สุดเทคนิคเหล่านี้ (และอื่น ๆ ) จะตกอยู่ภายใต้ "การคอมไพล์เวลา" แม้ว่าจะถูกนำไปใช้งานclass X : public Yก็ตาม


เกี่ยวกับโปรแรกของคุณสำหรับการคอมไพล์ไทม์สิ่งนี้เกี่ยวข้องกับหนึ่งในคำถามหลักของฉัน คุณเคยต้องการที่จะทำให้ชัดเจนว่าคุณต้องการที่จะทำงานกับอินเทอร์เฟซที่ชัดเจน กล่าวคือ 'ฉันไม่สนใจว่าคุณมีฟังก์ชั่นทั้งหมดที่ฉันต้องการหรือไม่ถ้าคุณไม่สืบทอดจาก Class Z ฉันก็ไม่ต้องการอะไรกับคุณ' นอกจากนี้การสืบทอดขณะใช้งานจะไม่สูญเสียข้อมูลประเภทเมื่อใช้ตัวชี้ / การอ้างอิงถูกต้องหรือไม่
Chris Morris

@ChrisMorris: ไม่ถ้ามันใช้งานได้แล้วมันก็ใช้งานได้ซึ่งเป็นสิ่งที่คุณควรใส่ใจ ทำไมทำให้บางคนเขียนรหัสเดียวกันที่อื่น ๆ
jmoreno

1
@ChrisMorris: ไม่ฉันไม่ต้องการ หากฉันต้องการ X เพียงอย่างเดียวนี่เป็นหนึ่งในหลักการพื้นฐานของการห่อหุ้มที่ฉันควรถามและดูแลเกี่ยวกับ X เท่านั้นนอกจากนี้มันจะสูญเสียข้อมูลประเภท ยกตัวอย่างเช่นคุณไม่สามารถจัดสรรสแต็กของวัตถุประเภทนี้ได้ คุณไม่สามารถสร้างแม่แบบได้เนื่องจากเป็นประเภทที่แท้จริง คุณไม่สามารถเรียกฟังก์ชันสมาชิก templated บนพวกเขา
DeadMG

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

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