Pertanyaan Apa motivasi di balik polimorfisme statis dalam C ++?


Saya memahami mekanika polimorfisme statis menggunakan Pola Template Berulang Beringat. Saya hanya tidak mengerti apa yang baik untuk itu.

Motivasi yang dinyatakan adalah:

Kami mengorbankan beberapa fleksibilitas polimorfisme dinamis untuk kecepatan.

Tapi kenapa repot-repot dengan sesuatu sangat rumit seperti:

template <class Derived>
class Base
{
public:
    void interface()
    {
         // ...
         static_cast<Derived*>(this)->implementation();
         // ...
    }
};

class Derived : Base<Derived>
{
private:
     void implementation();
};

Kapan Anda bisa melakukannya:

class Base
{
public: 
    void interface();
}

class Derived : public Base
{
public: 
    void interface();
}

Tebakan terbaik saya adalah bahwa tidak ada perbedaan semantik dalam kode dan itu hanya masalah gaya C ++ yang bagus.

Herb Sutter menulis Exceptional C++ style: Chapter 18 bahwa:

Lebih suka membuat fungsi virtual menjadi pribadi.

Disertai tentu saja dengan penjelasan yang menyeluruh mengapa hal ini terjadi gaya yang bagus.

Dalam konteks panduan ini, contoh pertama adalah baik, karena:

Itu void implementation() berfungsi dalam contoh dapat berpura-pura menjadi virtual, karena di sini untuk melakukan kustomisasi kelas. Karena itu harus pribadi.

Dan contoh kedua adalah buruk, sejak:

Kita seharusnya tidak ikut campur dengan antarmuka publik untuk melakukan kustomisasi.

Pertanyaanku adalah:

  1. Apa yang saya rindukan tentang polimorfisme statis? Apakah semuanya tentang gaya C ++ yang baik?
  2. Kapan seharusnya digunakan? Apa panduannya?

32
2017-09-28 02:59


asal


Jawaban:


Apa yang saya rindukan tentang polimorfisme statis? Apakah semuanya tentang gaya C ++ yang baik?

Polimorfisme statis dan polimorfisme runtime adalah hal yang berbeda dan mencapai tujuan yang berbeda. Mereka keduanya secara teknis polimorfisme, di mana mereka memutuskan potongan kode untuk mengeksekusi berdasarkan jenis sesuatu. Runtime polymorphism defers mengikat jenis sesuatu (dan dengan demikian kode yang berjalan) sampai runtime, sementara polimorfisme statis benar-benar diselesaikan pada waktu kompilasi.

Ini menghasilkan pro dan kontra untuk masing-masing. Misalnya, polimorfisme statis dapat memeriksa asumsi pada waktu kompilasi, atau memilih di antara opsi yang tidak akan dikompilasi. Ini juga menyediakan banyak informasi kepada compiler dan optimizer, yang dapat mengetahui sepenuhnya target panggilan dan informasi lainnya. Tetapi polimorfisme statis mensyaratkan bahwa implementasi tersedia untuk compiler untuk diperiksa di setiap unit terjemahan, dapat menyebabkan ukuran kode biner (template adalah salinan pasta celana mewah), dan jangan biarkan penentuan ini terjadi saat runtime.

Misalnya, pertimbangkan sesuatu seperti std::advance:

template<typename Iterator>
void advance(Iterator& it, ptrdiff_t offset)
{
    // If it is a random access iterator:
    // it += offset;
    // If it is a bidirectional iterator:
    // for (; offset < 0; ++offset) --it;
    // for (; offset > 0; --offset) ++it;
    // Otherwise:
    // for (; offset > 0; --offset) ++it;
}

Tidak ada cara untuk mengompilasi ini menggunakan polimorfisme runtime. Anda harus membuat keputusan pada waktu kompilasi. (Biasanya Anda akan melakukan ini dengan pengiriman tag misalnya)

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, random_access_iterator_tag)
{
    // Won't compile for bidirectional iterators!
    it += offset;
}

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, bidirectional_iterator_tag)
{
    // Works for random access, but slow
    for (; offset < 0; ++offset) --it; // Won't compile for forward iterators
    for (; offset > 0; --offset) ++it;
}

template<typename Iterator>
void advance_impl(Iterator& it, ptrdiff_t offset, forward_iterator_tag)
{
     // Doesn't allow negative indices! But works for forward iterators...
     for (; offset > 0; --offset) ++it;
}

template<typename Iterator>
void advance(Iterator& it, ptrdiff_t offset)
{
    // Use overloading to select the right one!
    advance_impl(it, offset, typename iterator_traits<Iterator>::iterator_category());
}  

Demikian pula, ada kasus di mana Anda benar-benar tidak tahu jenisnya pada waktu kompilasi. Mempertimbangkan:

void DoAndLog(std::ostream& out, int parameter)
{
    out << "Logging!";
}

Sini, DoAndLog tidak tahu apa-apa tentang yang sebenarnya ostream implementasi itu mendapat - dan mungkin tidak mungkin untuk menentukan secara statis jenis apa yang akan dilewati masuk Tentu, ini dapat diubah menjadi template:

template<typename StreamT>
void DoAndLog(StreamT& out, int parameter)
{
    out << "Logging!";
}

Tapi kekuatan ini DoAndLog untuk diimplementasikan dalam file header, yang mungkin tidak praktis. Ini juga mengharuskan semua kemungkinan implementasi StreamTterlihat pada waktu kompilasi, yang mungkin tidak benar - polimorfisme runtime dapat bekerja (meskipun ini tidak disarankan) di seluruh DLL atau SO batas.


Kapan seharusnya digunakan? Apa panduannya?

Ini seperti seseorang datang kepada Anda dan berkata "ketika saya sedang menulis sebuah kalimat, haruskah saya menggunakan kalimat majemuk atau kalimat sederhana"? Atau mungkin seorang pelukis berkata, "Haruskah saya selalu menggunakan cat merah atau cat biru?" Tidak ada jawaban yang benar, dan tidak ada aturan yang bisa diikuti secara membabi buta di sini. Anda harus melihat pro dan kontra dari masing-masing pendekatan, dan memutuskan peta mana yang terbaik untuk domain masalah khusus Anda.


Adapun CRTP, sebagian besar kasus penggunaan untuk itu adalah untuk memungkinkan kelas dasar menyediakan sesuatu dalam hal kelas turunan; misalnya Boost's iterator_facade. Kelas dasar perlu memiliki hal-hal seperti itu DerivedClass operator++() { /* Increment and return *this */ } di dalam - ditentukan dalam hal yang diturunkan dalam fungsi anggota tanda tangan.

Ini dapat digunakan untuk tujuan polimorfik, tetapi saya belum melihat terlalu banyak dari itu.


36
2017-09-28 03:30



Tautan yang Anda berikan menyebutkan meningkatkan iterator sebagai contoh polimorfisme statis. Iterator STL juga menunjukkan pola ini. Mari kita lihat contoh dan pertimbangkan mengapa penulis dari jenis tersebut memutuskan pola ini sesuai:

#include <vector>
#include <iostream>
using namespace std;
void print_ints( vector<int> const& some_ints )
{
    for( vector<int>::const_iterator i = some_ints.begin(), end = some_ints.end(); i != end; ++i )
    {
        cout << *i;
    }
}

Sekarang, bagaimana kita akan menerapkannya int vector<int>::const_iterator::operator*() const; Bisakah kita menggunakan polymprhism untuk ini? Yah, tidak. Apa yang akan menjadi tanda tangan dari fungsi virtual kami? void const* operator*() const? Itu tidak berguna! Tipe ini telah dihapus (terdegradasi dari int to void *). Sebaliknya, pola template berulang yang aneh melangkah masuk untuk membantu kami menghasilkan jenis iterator. Berikut ini perkiraan kasar kelas iterator yang perlu kami terapkan di atas:

template<typename T>
class const_iterator_base
{
public:
    const_iterator_base():{}

    T::contained_type const& operator*() const { return Ptr(); }
    T::contained_type const& operator->() const { return Ptr(); }
    // increment, decrement, etc, can be implemented and forwarded to T
    // ....
private:
    T::contained_type const* Ptr() const { return static_cast<T>(this)->Ptr(); }
};

Polimorfisme dinamis tradisional tidak dapat memberikan implementasi di atas!

Istilah terkait dan penting adalah polimorfisme parametrik. Ini memungkinkan Anda menerapkan API serupa di, katakanlah, python bahwa Anda dapat menggunakan pola template berulang yang aneh di C ++. Semoga ini bermanfaat!

Saya pikir itu layak untuk ditusuk dari sumber semua kerumitan ini, dan mengapa bahasa seperti Java dan C # kebanyakan mencoba menghindarinya: ketik penghapusan! Dalam c ++ tidak ada yang mengandung semua yang berguna Object ketik dengan informasi yang berguna. Sebaliknya kita punya void* dan begitu sudah void* Anda benar-benar tidak memiliki apa-apa! Jika Anda memiliki antarmuka yang meluruh void* satu-satunya cara untuk pulih adalah dengan membuat asumsi berbahaya atau menyimpan informasi jenis ekstra.


3
2017-09-28 03:46



Meskipun mungkin ada kasus di mana polimorfisme statis berguna (jawaban yang lain telah mencantumkan beberapa), saya biasanya melihatnya sebagai hal yang buruk. Mengapa? Karena Anda tidak dapat benar-benar menggunakan pointer ke kelas dasar lagi, Anda selalu harus memberikan argumen template yang menyediakan jenis turunan yang tepat. Dan dalam hal ini, Anda juga bisa menggunakan tipe turunan secara langsung. Dan, untuk membuatnya terus terang, polimorfisme statis bukanlah tentang orientasi objek.

Perbedaan runtime antara polimorfisme statis dan dinamis adalah persis dua pointer dereferenciations (iff compiler benar-benar inline metode pengiriman di kelas dasar, jika tidak karena alasan tertentu, polimorfisme statis lebih lambat). Itu tidak terlalu mahal, terutama karena pencarian kedua seharusnya selalu memukul cache. Semua dalam semua, mereka biasanya lebih murah daripada fungsi panggilan itu sendiri, dan tentu saja layak untuk mendapatkan fleksibilitas nyata yang disediakan oleh polimorfisme dinamis.


1
2017-09-28 07:29