@ Sbi: Jika dia melakukan itu, dia akan menemukan pertanyaannya sendiri. Dan itu akan aneh berulang. :)
Craig McQueen
1
BTW, menurut saya istilah itu seharusnya "berulang berulang". Apakah saya salah memahami maknanya?
Craig McQueen
1
Craig: Saya pikir Anda adalah; "anehnya berulang" dalam arti ditemukan muncul dalam berbagai konteks.
Gareth McCaughan
Jawaban:
276
Singkatnya, CRTP adalah ketika sebuah kelas Amemiliki kelas dasar yang merupakan spesialisasi template untuk kelas Aitu sendiri. Misalnya
template<class T>class X{...};class A :public X<A>{...};
Hal ini anehnya berulang, bukan? :)
Sekarang, apa ini memberi Anda? Ini benar-benar memberi Xtemplat kemampuan untuk menjadi kelas dasar untuk spesialisasinya.
Misalnya, Anda bisa membuat kelas singleton generik (versi sederhana) seperti ini
template<classActualClass>classSingleton{public:staticActualClass&GetInstance(){if(p ==nullptr)
p =newActualClass;return*p;}protected:staticActualClass* p;private:Singleton(){}Singleton(Singletonconst&);Singleton&operator=(Singletonconst&);};template<class T>
T*Singleton<T>::p =nullptr;
Sekarang, untuk membuat kelas arbitrer Amenjadi lajang Anda harus melakukan ini
class A:publicSingleton<A>{//Rest of functionality for class A};
Jadi kamu melihat? Templat tunggal mengasumsikan bahwa spesialisasi untuk jenis apa pun Xakan diwarisi dari singleton<X>dan dengan demikian semua anggota (publik, yang dilindungi) dapat diakses, termasukGetInstance ! Ada kegunaan lain yang berguna dari CRTP. Misalnya, jika Anda ingin menghitung semua instance yang saat ini ada untuk kelas Anda, tetapi ingin merangkum logika ini dalam templat terpisah (ide untuk kelas konkret cukup sederhana - memiliki variabel statis, kenaikan dalam ctors, penurunan dalam dtors ). Cobalah untuk melakukannya sebagai latihan!
Contoh lain yang bermanfaat, untuk Boost (saya tidak yakin bagaimana mereka menerapkannya, tetapi CRTP juga akan melakukannya). Bayangkan Anda hanya ingin menyediakan operator <untuk kelas Anda, tetapi secara otomatis operator ==untuk mereka!
Anda bisa melakukannya seperti ini:
template<classDerived>classEquality{};template<classDerived>booloperator==(Equality<Derived>const& op1,Equality<Derived>const& op2){Derivedconst& d1 =static_cast<Derivedconst&>(op1);//you assume this works //because you know that the dynamic type will actually be your template parameter.//wonderful, isn't it?Derivedconst& d2 =static_cast<Derivedconst&>(op2);return!(d1 < d2)&&!(d2 < d1);//assuming derived has operator <}
Hal ini bisa terlihat bahwa Anda akan menulis kurang jika Anda hanya menulis Operator ==untuk Apple, tapi bayangkan bahwa Equalitytemplate yang akan memberikan tidak hanya ==tetapi >, >=, <=dll Dan Anda bisa menggunakan definisi ini untuk beberapa kelas, menggunakan kembali kode!
Posting ini tidak menganjurkan singleton sebagai pola pemrograman yang baik. Ini hanya menggunakannya sebagai ilustrasi yang dapat dipahami secara umum. Kembali ke-1 tidak beralasan
John Dibling
3
@Armen: Jawabannya menjelaskan CRTP dengan cara yang dapat dipahami dengan jelas, ini jawaban yang bagus, terima kasih atas jawaban yang bagus.
Alok Simpan
1
@Armen: terima kasih atas penjelasan yang bagus ini. Saya semacam tidak pernah mendapatkan CRTP sebelumnya, tetapi contoh kesetaraan telah menerangi! +1
Paul
1
Contoh lain dari penggunaan CRTP adalah ketika Anda membutuhkan kelas yang tidak dapat disalin: templat <class T> class NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & operator = (const NonCopyable &); }; Kemudian Anda menggunakan noncopyable seperti di bawah ini: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren
2
@Puppy: Singleton tidak buruk. Ini terlalu sering digunakan oleh programmer di bawah rata-rata ketika pendekatan lain akan lebih tepat, tetapi sebagian besar penggunaannya mengerikan tidak membuat pola itu sendiri mengerikan. Ada kasus di mana singleton adalah pilihan terbaik, meskipun itu jarang terjadi.
Kaiserludi
47
Di sini Anda dapat melihat contoh yang bagus. Jika Anda menggunakan metode virtual, program akan tahu apa yang dieksekusi di runtime. Mengimplementasikan CRTP, kompilerlah yang menentukan waktu kompilasi !!! Ini adalah kinerja yang luar biasa!
template<class T>classWriter{public:Writer(){}~Writer(){}void write(constchar* str)const{static_cast<const T*>(this)->writeImpl(str);//here the magic is!!!}};classFileWriter:publicWriter<FileWriter>{public:FileWriter(FILE* aFile){ mFile = aFile;}~FileWriter(){ fclose(mFile);}//here comes the implementation of the write method on the subclassvoid writeImpl(constchar* str)const{
fprintf(mFile,"%s\n", str);}private:FILE* mFile;};classConsoleWriter:publicWriter<ConsoleWriter>{public:ConsoleWriter(){}~ConsoleWriter(){}void writeImpl(constchar* str)const{
printf("%s\n", str);}};
Tidak bisakah Anda melakukan ini dengan mendefinisikan virtual void write(const char* str) const = 0;? Meskipun harus adil, teknik ini tampaknya sangat membantu ketika writemelakukan pekerjaan lain.
atlex2
26
Menggunakan metode virtual murni Anda menyelesaikan warisan dalam runtime alih-alih waktu kompilasi. CRTP digunakan untuk menyelesaikan ini dalam waktu kompilasi sehingga eksekusi akan lebih cepat.
GutiMac
1
Cobalah membuat fungsi sederhana yang mengharapkan Writer abstrak: Anda tidak dapat melakukannya karena tidak ada kelas bernama Writer di mana pun, jadi di mana tepatnya polimorfisme Anda? Ini tidak setara dengan fungsi virtual sama sekali dan ini jauh kurang berguna.
22
CRTP adalah teknik untuk menerapkan polimorfisme waktu kompilasi. Ini contoh yang sangat sederhana. Dalam contoh di bawah ini, ProcessFoo()bekerja dengan Baseantarmuka kelas dan Base::Foomemanggil metode objek turunan foo(), yang adalah apa yang ingin Anda lakukan dengan metode virtual.
Mungkin juga ada manfaatnya dalam contoh ini untuk menambahkan contoh tentang cara mengimplementasikan foo () default di kelas Base yang akan dipanggil jika tidak ada Derived yang mengimplementasikannya. AKA mengubah foo di Base ke beberapa nama lain (misal penelepon ()), menambahkan fungsi foo () baru ke Base yang menyebutkan "Base". Kemudian panggil pemanggil () di dalam ProcessFoo
wizurd
@wizurd Contoh ini lebih untuk menggambarkan fungsi kelas dasar virtual murni yaitu kita menegakkan yang foo()diimplementasikan oleh kelas turunan.
kulit blues
3
Ini adalah jawaban favorit saya, karena ini juga menunjukkan mengapa pola ini berguna dengan ProcessFoo()fungsinya.
Pietro
Saya tidak mendapatkan poin dari kode ini, karena dengan void ProcessFoo(T* b)dan tanpa Derived dan AnotherDerived sebenarnya diturunkan masih akan berfungsi. IMHO akan lebih menarik jika ProcessFoo tidak memanfaatkan template.
Gabriel Devillers
1
@GabrielDevillers Pertama, templatized ProcessFoo()akan bekerja dengan semua jenis yang mengimplementasikan antarmuka yaitu dalam hal ini tipe input T harus memiliki metode yang disebut foo(). Kedua, untuk mendapatkan non-templatized ProcessFoountuk bekerja dengan banyak jenis, Anda mungkin akan akhirnya menggunakan RTTI yang ingin kita hindari. Selain itu, versi templatized memberi Anda waktu kompilasi memeriksa pada antarmuka.
kulit blues
6
Ini bukan jawaban langsung, melainkan contoh bagaimana CRTP dapat bermanfaat.
Contoh konkret yang baik dari CRTP adalah std::enable_shared_from_thisdari C ++ 11:
Kelas Tdapat mewarisi dari enable_shared_from_this<T>untuk mewarisi shared_from_thisfungsi anggota yang mendapatkan shared_ptrinstance menunjuk ke *this.
Artinya, mewarisi dari std::enable_shared_from_thismemungkinkan untuk mendapatkan pointer yang dibagikan (atau lemah) ke instance Anda tanpa akses ke sana (misalnya dari fungsi anggota di mana Anda hanya tahu tentang*this ).
Ini berguna ketika Anda perlu memberikan std::shared_ptrtetapi Anda hanya memiliki akses ke *this:
Alasan Anda tidak bisa thislangsung lulus bukan shared_from_this()karena itu akan merusak mekanisme kepemilikan:
struct S
{
std::shared_ptr<S> get_shared()const{return std::shared_ptr<S>(this);}};// Both shared_ptr think they're the only owner of S.// This invokes UB (double-free).
std::shared_ptr<S> s1 = std::make_shared<S>();
std::shared_ptr<S> s2 = s1->get_shared();
assert(s2.use_count()==1);
maaf, static_cast saya yang buruk menangani perubahan. Jika Anda ingin melihat kasus sudut tetap meskipun itu tidak menyebabkan kesalahan lihat di sini: ideone.com/LPkktf
odinthenerd
30
Contoh buruk Kode ini dapat dilakukan tanpa vtables tanpa menggunakan CRTP. Apa yang vtablesebenarnya disediakan adalah menggunakan kelas dasar (pointer atau referensi) untuk memanggil metode turunan. Anda harus menunjukkan cara melakukannya dengan CRTP di sini.
Etherealone
17
Dalam contoh Anda, Base<>::method ()bahkan tidak dipanggil, Anda juga tidak menggunakan polimorfisme di mana pun.
MikeMB
1
@Jichao, menurut @MikeMB 's catatan, Anda harus memanggil methodImpldalam methoddari Basedan di kelas turunan nama methodImplbukanmethod
Ivan Kush
1
jika Anda menggunakan metode yang sama () maka itu terikat secara statis dan Anda tidak perlu kelas dasar umum. Karena bagaimanapun Anda tidak dapat menggunakannya secara polimorf melalui pointer kelas dasar atau ref. Jadi kodenya akan terlihat seperti ini: #sertakan template <iostream> <typename T> struct Writer {void write () {static_cast <T *> (this) -> writeImpl (); }}; struct Derived1: public Writer <Derived1> {void writeImpl () {std :: cout << "D1"; }}; struct Derived2: public Writer <Derived2> {void writeImpl () {std :: cout << "DER2"; }};
Jawaban:
Singkatnya, CRTP adalah ketika sebuah kelas
A
memiliki kelas dasar yang merupakan spesialisasi template untuk kelasA
itu sendiri. MisalnyaHal ini anehnya berulang, bukan? :)
Sekarang, apa ini memberi Anda? Ini benar-benar memberi
X
templat kemampuan untuk menjadi kelas dasar untuk spesialisasinya.Misalnya, Anda bisa membuat kelas singleton generik (versi sederhana) seperti ini
Sekarang, untuk membuat kelas arbitrer
A
menjadi lajang Anda harus melakukan iniJadi kamu melihat? Templat tunggal mengasumsikan bahwa spesialisasi untuk jenis apa pun
X
akan diwarisi darisingleton<X>
dan dengan demikian semua anggota (publik, yang dilindungi) dapat diakses, termasukGetInstance
! Ada kegunaan lain yang berguna dari CRTP. Misalnya, jika Anda ingin menghitung semua instance yang saat ini ada untuk kelas Anda, tetapi ingin merangkum logika ini dalam templat terpisah (ide untuk kelas konkret cukup sederhana - memiliki variabel statis, kenaikan dalam ctors, penurunan dalam dtors ). Cobalah untuk melakukannya sebagai latihan!Contoh lain yang bermanfaat, untuk Boost (saya tidak yakin bagaimana mereka menerapkannya, tetapi CRTP juga akan melakukannya). Bayangkan Anda hanya ingin menyediakan operator
<
untuk kelas Anda, tetapi secara otomatis operator==
untuk mereka!Anda bisa melakukannya seperti ini:
Sekarang Anda bisa menggunakannya seperti ini
Sekarang, Anda belum menyediakan operator secara eksplisit
==
untukApple
? Tetapi Anda memilikinya! Kamu bisa menulisHal ini bisa terlihat bahwa Anda akan menulis kurang jika Anda hanya menulis Operator
==
untukApple
, tapi bayangkan bahwaEquality
template yang akan memberikan tidak hanya==
tetapi>
,>=
,<=
dll Dan Anda bisa menggunakan definisi ini untuk beberapa kelas, menggunakan kembali kode!CRTP adalah hal yang luar biasa :) HTH
sumber
Di sini Anda dapat melihat contoh yang bagus. Jika Anda menggunakan metode virtual, program akan tahu apa yang dieksekusi di runtime. Mengimplementasikan CRTP, kompilerlah yang menentukan waktu kompilasi !!! Ini adalah kinerja yang luar biasa!
sumber
virtual void write(const char* str) const = 0;
? Meskipun harus adil, teknik ini tampaknya sangat membantu ketikawrite
melakukan pekerjaan lain.CRTP adalah teknik untuk menerapkan polimorfisme waktu kompilasi. Ini contoh yang sangat sederhana. Dalam contoh di bawah ini,
ProcessFoo()
bekerja denganBase
antarmuka kelas danBase::Foo
memanggil metode objek turunanfoo()
, yang adalah apa yang ingin Anda lakukan dengan metode virtual.http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e
Keluaran:
sumber
foo()
diimplementasikan oleh kelas turunan.ProcessFoo()
fungsinya.void ProcessFoo(T* b)
dan tanpa Derived dan AnotherDerived sebenarnya diturunkan masih akan berfungsi. IMHO akan lebih menarik jika ProcessFoo tidak memanfaatkan template.ProcessFoo()
akan bekerja dengan semua jenis yang mengimplementasikan antarmuka yaitu dalam hal ini tipe input T harus memiliki metode yang disebutfoo()
. Kedua, untuk mendapatkan non-templatizedProcessFoo
untuk bekerja dengan banyak jenis, Anda mungkin akan akhirnya menggunakan RTTI yang ingin kita hindari. Selain itu, versi templatized memberi Anda waktu kompilasi memeriksa pada antarmuka.Ini bukan jawaban langsung, melainkan contoh bagaimana CRTP dapat bermanfaat.
Contoh konkret yang baik dari CRTP adalah
std::enable_shared_from_this
dari C ++ 11:Artinya, mewarisi dari
std::enable_shared_from_this
memungkinkan untuk mendapatkan pointer yang dibagikan (atau lemah) ke instance Anda tanpa akses ke sana (misalnya dari fungsi anggota di mana Anda hanya tahu tentang*this
).Ini berguna ketika Anda perlu memberikan
std::shared_ptr
tetapi Anda hanya memiliki akses ke*this
:Alasan Anda tidak bisa
this
langsung lulus bukanshared_from_this()
karena itu akan merusak mekanisme kepemilikan:sumber
Sama seperti catatan:
CRTP dapat digunakan untuk mengimplementasikan polimorfisme statis (yang menyukai polimorfisme dinamis tetapi tanpa tabel penunjuk fungsi virtual).
Outputnya adalah:
sumber
vtable
s tanpa menggunakan CRTP. Apa yangvtable
sebenarnya disediakan adalah menggunakan kelas dasar (pointer atau referensi) untuk memanggil metode turunan. Anda harus menunjukkan cara melakukannya dengan CRTP di sini.Base<>::method ()
bahkan tidak dipanggil, Anda juga tidak menggunakan polimorfisme di mana pun.methodImpl
dalammethod
dariBase
dan di kelas turunan namamethodImpl
bukanmethod