Antarmuka implisit vs eksplisit

9

Saya pikir saya mengerti keterbatasan sebenarnya dari polimorfisme waktu kompilasi dan polimorfisme run-time. Tapi apa perbedaan konseptual antara antarmuka eksplisit (run-time polymorphism. Yaitu fungsi virtual dan pointer / referensi) dan antarmuka implisit (compile-time polymorphism, mis. Templates) .

Pikiranku adalah bahwa dua objek yang menawarkan antarmuka eksplisit yang sama harus memiliki tipe objek yang sama (atau memiliki leluhur yang sama), sedangkan dua objek yang menawarkan antarmuka implisit yang sama tidak perlu jenis objek yang sama, dan, tidak termasuk implisit antarmuka yang mereka berdua tawarkan, dapat memiliki fungsi yang sangat berbeda.

Ada pemikiran tentang ini?

Dan jika dua objek menawarkan antarmuka implisit yang sama, alasan apa (di samping manfaat teknis tidak memerlukan pengiriman dinamis dengan tabel fungsi pencarian virtual, dll) yang ada karena tidak memiliki benda-benda ini mewarisi dari objek dasar yang menyatakan antarmuka itu, dengan demikian menjadikannya antarmuka yang eksplisit ? Cara lain untuk mengatakannya: dapatkah Anda memberi saya contoh di mana dua objek yang menawarkan antarmuka implisit yang sama (dan karenanya dapat digunakan sebagai tipe ke kelas templat contoh) tidak boleh mewarisi dari kelas dasar yang membuat antarmuka itu eksplisit?

Beberapa pos terkait:


Berikut ini contoh untuk menjadikan pertanyaan ini lebih konkret:

Antarmuka Tersirat:

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

Antarmuka Eksplisit:

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

Contoh konkret yang lebih mendalam:

Beberapa masalah C ++ dapat diselesaikan dengan:

  1. kelas templated yang tipe templatnya menyediakan antarmuka implisit
  2. kelas non-templated yang mengambil pointer kelas dasar yang menyediakan antarmuka eksplisit

Kode yang tidak berubah:

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

Kasus 1 . Kelas non-templated yang mengambil pointer kelas dasar yang menyediakan antarmuka eksplisit:

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

Kasus 2 . Kelas templated yang tipe templatnya menyediakan antarmuka implisit:

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

Kasus 3 . Kelas templated yang tipe templatnya menyediakan antarmuka implisit (kali ini, bukan berasal dari 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;
}

Kasus 1 mengharuskan objek yang dilewatkan useCoolClass()menjadi anak dari CoolClass(dan mengimplementasikan worthless()). Kasus 2 dan 3, di sisi lain, akan mengambil kelas apa pun yang memiliki doSomethingCool()fungsi.

Jika pengguna kode selalu subclass baik-baik saja CoolClass, maka Kasus 1 masuk akal secara intuitif, karena CoolClassUserakan selalu mengharapkan implementasi dari a CoolClass. Tetapi anggap kode ini akan menjadi bagian dari kerangka kerja API, jadi saya tidak dapat memprediksi apakah pengguna ingin membuat subkelas CoolClassatau memutar kelas mereka sendiri yang memiliki doSomethingCool()fungsi.

Chris Morris
sumber
Mungkin saya kehilangan sesuatu, tetapi bukankah perbedaan penting sudah secara ringkas dinyatakan dalam paragraf pertama Anda, yaitu bahwa antarmuka eksplisit adalah run-time polimorfisme, sedangkan antarmuka implisit adalah kompilasi-waktu polimorfisme?
Robert Harvey
2
Ada beberapa masalah yang dapat diselesaikan dengan memiliki Kelas atau fungsi yang membawa pointer ke Kelas abstrak (yang menyediakan antarmuka eksplisit), atau dengan memiliki Kelas templated atau fungsi yang menggunakan objek yang menyediakan antarmuka implisit. Kedua solusi bekerja. Kapan Anda ingin menggunakan solusi pertama? Kedua?
Chris Morris
Saya pikir sebagian besar pertimbangan ini berantakan ketika Anda membuka konsep sedikit lagi. misalnya, di mana Anda akan cocok dengan polimorfisme statis non-warisan?
Javier

Jawaban:

8

Anda telah mendefinisikan poin penting-satu adalah run-time dan yang lainnya adalah compile-time . Informasi nyata yang Anda butuhkan adalah konsekuensi dari pilihan ini.

Kompositime:

  • Pro: Antarmuka waktu kompilasi jauh lebih granular daripada yang dijalankan waktu. Maksud saya, Anda hanya dapat menggunakan persyaratan dari satu fungsi, atau satu set fungsi, sebagaimana Anda menyebutnya. Anda tidak harus selalu melakukan seluruh antarmuka. Persyaratannya hanya dan persis apa yang Anda butuhkan.
  • Pro: Teknik seperti CRTP berarti bahwa Anda dapat menggunakan antarmuka implisit untuk implementasi default hal-hal seperti operator. Anda tidak akan pernah bisa melakukan hal seperti itu dengan warisan run-time.
  • Pro: Antarmuka implisit jauh lebih mudah untuk membuat dan melipatgandakan "warisan" daripada antarmuka run-time, dan jangan memaksakan segala jenis pembatasan biner - misalnya, kelas POD dapat menggunakan antarmuka implisit. Tidak perlu virtualwarisan atau gangguan lain dengan antarmuka implisit - keuntungan besar.
  • Pro: Kompilator dapat melakukan lebih banyak optimasi untuk antarmuka waktu kompilasi. Selain itu, keamanan tipe ekstra membuat kode lebih aman.
  • Pro: Tidak mungkin melakukan pengetikan nilai untuk antarmuka run-time, karena Anda tidak tahu ukuran atau keselarasan objek akhir. Ini berarti bahwa setiap kasus yang membutuhkan / mendapat manfaat dari pengetikan nilai akan mendapatkan manfaat besar dari templat.
  • Con: Templat adalah sundal untuk dikompilasi dan digunakan, dan mereka dapat secara fiddly memindahkan antar kompiler
  • Con: Templat tidak dapat dimuat pada saat run-time (jelas), sehingga mereka memiliki batasan dalam mengekspresikan struktur data dinamis, misalnya.

Runtime:

  • Pro: Jenis terakhir tidak harus diputuskan sampai run-time. Ini berarti bahwa run-time inheritance dapat mengekspresikan beberapa struktur data dengan lebih mudah, jika templat dapat melakukannya sama sekali. Anda juga dapat mengekspor tipe polimorfik run-time melintasi batas C, misalnya, COM.
  • Pro: Jauh lebih mudah untuk menentukan dan menerapkan warisan run-time, dan Anda tidak akan benar-benar mendapatkan perilaku spesifik kompiler.
  • Con: Run-time inheritance bisa lebih lambat daripada compile-time inheritane.
  • Con: Warisan run-time kehilangan informasi jenis.
  • Con: Run-time inheritance jauh lebih tidak fleksibel.
  • Con: Multiple inheritance adalah menyebalkan.

Diberikan daftar relatif, jika Anda tidak memerlukan keunggulan spesifik run-time inheritance, jangan gunakan itu. Ini lebih lambat, kurang fleksibel, dan kurang aman dibandingkan template.

Sunting: Perlu dicatat bahwa di C ++ khususnya ada kegunaan untuk pewarisan lainnya dari run-time polymorphism. Misalnya, Anda bisa mewarisi typedef, atau menggunakannya untuk menandai, atau menggunakan CRTP. Namun, pada akhirnya, teknik-teknik ini (dan lainnya) benar-benar berada di bawah "Waktu kompilasi", meskipun mereka diimplementasikan menggunakan class X : public Y.

DeadMG
sumber
Mengenai pro pertama Anda untuk waktu yang bersamaan, ini terkait dengan salah satu pertanyaan utama saya. Apakah Anda ingin membuatnya jelas bahwa Anda hanya ingin bekerja dengan antarmuka yang eksplisit. Yaitu. 'Saya tidak peduli jika Anda memiliki semua fungsi yang saya butuhkan, jika Anda tidak mewarisi dari Kelas Z maka saya tidak ingin ada hubungannya dengan Anda'. Juga, pewarisan run-time tidak kehilangan info jenis saat menggunakan pointer / referensi, benar?
Chris Morris
@ChrisMorris: Tidak. Jika berhasil maka berfungsi, itu yang harus Anda pedulikan. Mengapa membuat seseorang menulis kode yang sama persis di tempat lain?
jmoreno
1
@ ChrisMorris: Tidak, saya tidak akan. Jika saya hanya membutuhkan X, maka itu adalah salah satu prinsip dasar dasar enkapsulasi yang seharusnya saya hanya minta dan pedulikan tentang X. Selain itu, itu juga kehilangan informasi jenis. Anda tidak dapat, misalnya, menumpuk mengalokasikan objek jenis seperti itu. Anda tidak dapat membuat contoh template dengan tipe yang sebenarnya. Anda tidak dapat memanggil fungsi anggota templated pada fungsi tersebut.
DeadMG
Bagaimana dengan situasi di mana Anda memiliki kelas Q yang menggunakan beberapa kelas. Q mengambil parameter templat, jadi setiap kelas yang menyediakan antarmuka implisit akan melakukannya, atau begitulah menurut kami. Ternyata kelas Q juga mengharapkan kelas internal (sebut saja H) untuk menggunakan antarmuka Q. Misalnya, ketika objek H dirusak, ia harus memanggil beberapa fungsi Q's. Ini tidak dapat ditentukan dalam antarmuka implisit. Dengan demikian, templat gagal. Lebih jelasnya, seperangkat kelas ketat yang membutuhkan lebih dari sekedar antarmuka implisit satu sama lain tampaknya menghalangi penggunaan template.
Chris Morris
Con compiletime: Jelek untuk debug, perlunya memasukkan definisi ke dalam header
JFFIGK