Apakah pola templat berulang yang anehnya muncul?

187

Tanpa merujuk ke buku, dapatkah orang memberikan penjelasan yang baik CRTPdengan contoh kode?

Alok Simpan
sumber
2
Baca pertanyaan CRTP di SO: stackoverflow.com/questions/tagged/crtp . Itu mungkin memberi Anda beberapa ide.
sbi
68
@ 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 <class ActualClass> 
class Singleton
{
   public:
     static ActualClass& GetInstance()
     {
       if(p == nullptr)
         p = new ActualClass;
       return *p; 
     }

   protected:
     static ActualClass* p;
   private:
     Singleton(){}
     Singleton(Singleton const &);
     Singleton& operator = (Singleton const &); 
};
template <class T>
T* Singleton<T>::p = nullptr;

Sekarang, untuk membuat kelas arbitrer Amenjadi lajang Anda harus melakukan ini

class A: public Singleton<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<class Derived>
class Equality
{
};

template <class Derived>
bool operator == (Equality<Derived> const& op1, Equality<Derived> const & op2)
{
    Derived const& d1 = static_cast<Derived const&>(op1);//you assume this works     
    //because you know that the dynamic type will actually be your template parameter.
    //wonderful, isn't it?
    Derived const& d2 = static_cast<Derived const&>(op2); 
    return !(d1 < d2) && !(d2 < d1);//assuming derived has operator <
}

Sekarang Anda bisa menggunakannya seperti ini

struct Apple:public Equality<Apple> 
{
    int size;
};

bool operator < (Apple const & a1, Apple const& a2)
{
    return a1.size < a2.size;
}

Sekarang, Anda belum menyediakan operator secara eksplisit ==untuk Apple? Tetapi Anda memilikinya! Kamu bisa menulis

int main()
{
    Apple a1;
    Apple a2; 

    a1.size = 10;
    a2.size = 10;
    if(a1 == a2) //the compiler won't complain! 
    {
    }
}

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!

CRTP adalah hal yang luar biasa :) HTH

Armen Tsirunyan
sumber
62
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>
class Writer
{
  public:
    Writer()  { }
    ~Writer()  { }

    void write(const char* str) const
    {
      static_cast<const T*>(this)->writeImpl(str); //here the magic is!!!
    }
};


class FileWriter : public Writer<FileWriter>
{
  public:
    FileWriter(FILE* aFile) { mFile = aFile; }
    ~FileWriter() { fclose(mFile); }

    //here comes the implementation of the write method on the subclass
    void writeImpl(const char* str) const
    {
       fprintf(mFile, "%s\n", str);
    }

  private:
    FILE* mFile;
};


class ConsoleWriter : public Writer<ConsoleWriter>
{
  public:
    ConsoleWriter() { }
    ~ConsoleWriter() { }

    void writeImpl(const char* str) const
    {
      printf("%s\n", str);
    }
};
GutiMac
sumber
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.

http://coliru.stacked-crooked.com/a/2d27f1e09d567d0e

template <typename T>
struct Base {
  void foo() {
    (static_cast<T*>(this))->foo();
  }
};

struct Derived : public Base<Derived> {
  void foo() {
    cout << "derived foo" << endl;
  }
};

struct AnotherDerived : public Base<AnotherDerived> {
  void foo() {
    cout << "AnotherDerived foo" << endl;
  }
};

template<typename T>
void ProcessFoo(Base<T>* b) {
  b->foo();
}


int main()
{
    Derived d1;
    AnotherDerived d2;
    ProcessFoo(&d1);
    ProcessFoo(&d2);
    return 0;
}

Keluaran:

derived foo
AnotherDerived foo
kulit biru
sumber
1
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:

[util.smartptr.enab] / 1

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:

struct Node;

void process_node(const std::shared_ptr<Node> &);

struct Node : std::enable_shared_from_this<Node> // CRTP
{
    std::weak_ptr<Node> parent;
    std::vector<std::shared_ptr<Node>> children;

    void add_child(std::shared_ptr<Node> child)
    {
        process_node(shared_from_this()); // Shouldn't pass `this` directly.
        child->parent = weak_from_this(); // Ditto.
        children.push_back(std::move(child));
    }
};

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);
Mário Feroldi
sumber
5

Sama seperti catatan:

CRTP dapat digunakan untuk mengimplementasikan polimorfisme statis (yang menyukai polimorfisme dinamis tetapi tanpa tabel penunjuk fungsi virtual).

#pragma once
#include <iostream>
template <typename T>
class Base
{
    public:
        void method() {
            static_cast<T*>(this)->method();
        }
};

class Derived1 : public Base<Derived1>
{
    public:
        void method() {
            std::cout << "Derived1 method" << std::endl;
        }
};


class Derived2 : public Base<Derived2>
{
    public:
        void method() {
            std::cout << "Derived2 method" << std::endl;
        }
};


#include "crtp.h"
int main()
{
    Derived1 d1;
    Derived2 d2;
    d1.method();
    d2.method();
    return 0;
}

Outputnya adalah:

Derived1 method
Derived2 method
Jichao
sumber
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"; }};
barney