Merakla yinelenen şablon modeli (CRTP) nedir?


187

Bir kitaba başvurmadan, lütfen CRTPbir kod örneği ile iyi bir açıklama yapabilir misiniz?


2
SO ile ilgili CRTP sorularını okuyun: stackoverflow.com/questions/tagged/crtp . Bu size bir fikir verebilir.
sbi

68
@sbi: Eğer bunu yaparsa, kendi sorusunu bulacaktır. Ve bu merakla tekrarlanacaktı. :)
Craig McQueen

1
BTW, bana öyle geliyor ki terim "merakla tekrar ediyor". Anlamı yanlış mı anlıyorum?
Craig McQueen

1
Craig: Bence sen; birden fazla bağlamda yetiştiği anlamında “merakla tekrarlanıyor”.
Gareth McCaughan

Yanıtlar:


275

Kısacası, CRTP, bir sınıfın A, sınıfın Akendisi için bir şablon uzmanlığı olan temel bir sınıfı olduğu zamandır . Örneğin

template <class T> 
class X{...};
class A : public X<A> {...};

O edilir merakla, değil mi yinelenen? :)

Şimdi, bu size ne veriyor? Bu aslında Xşablona uzmanlıkları için bir temel sınıf olma yeteneği verir .

Örneğin, böyle bir genel singleton sınıfı (basitleştirilmiş sürüm) yapabilirsiniz

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;

Şimdi, keyfi bir sınıfı Atekil yapmak için bunu yapmalısınız

class A: public Singleton<A>
{
   //Rest of functionality for class A
};

Gördün mü? Singleton şablonu, herhangi bir tür için uzmanlığının Xmiras singleton<X>alınacağını ve böylece de dahil olmak üzere tüm (genel, korunan) üyelerin erişilebilir olmasını varsayar GetInstance. CRTP'nin başka yararlı kullanımları da vardır. Örneğin, şu anda sınıfınız için var olan tüm örnekleri saymak istiyorsanız, ancak bu mantığı ayrı bir şablonda kapsüllemek istiyorsanız (somut bir sınıf fikri oldukça basittir - statik bir değişkene sahip olmak, aktörlerde artış, dtors'ta azalma ). Bir egzersiz olarak yapmaya çalışın!

Boost için yine bir başka yararlı örnek (nasıl uyguladıklarından emin değilim, ancak CRTP de yapacak). <Sınıflarınız için yalnızca operatör sağlamak ==, onlar için de otomatik olarak operatör sağlamak istediğinizi düşünün !

şöyle yapabilirsin:

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

Şimdi bu şekilde kullanabilirsiniz

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

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

Şimdi, açıkça operatörü sunmadınız ==için Apple? Ama sende var! Yazabilirsin

int main()
{
    Apple a1;
    Apple a2; 

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

Bu size sadece operatörü yazdıysam az yazacağını görünebilir ==için Apple, ama bu hayal Equalityşablon sadece sağlayacak ==ama >, >=, <=vs Ve bu tanımları kullanabilirsiniz çoklu kod yeniden, sınıflar!

CRTP harika bir şey :) HTH


61
Bu yazı, singleton'u iyi bir programlama
deseni olarak savunmuyor. Basitçe

3
@Armen: Cevap, CRTP'yi açıkça anlaşılabilecek şekilde açıklıyor, hoş bir cevap, böyle güzel bir cevap için teşekkürler.
Alok Kaydet

1
@Armen: Bu harika açıklama için teşekkürler. Daha önce CRTP alıyordum, ama eşitlik örneği aydınlatıcıydı! +1
Paul

1
CRTP kullanımına bir başka örnek de, kopyalanamayan bir sınıfa ihtiyaç duyduğunuz zamandır: <sınıf T> şablonu NonCopyable {korumalı: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); Kopyalanamaz & operatör = (const Kopyalanamaz &); }; Daha sonra aşağıdaki şekilde kullanılamaz: sınıf Mutex: özel NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren

2
@Puppy: Singleton korkunç değil. Diğer yaklaşımların daha uygun olacağı ortalama programcılar tarafından gereğinden fazla kullanılır, ancak kullanımlarının çoğunun korkunç olması, kalıbın kendisini korkunç yapmaz. Singleton'ın en iyi seçenek olduğu durumlar vardır, ancak bunlar nadirdir.
Kaiserludi

47

Burada harika bir örnek görebilirsiniz. Sanal yöntem kullanırsanız, program çalışma zamanında neyin yürütüldüğünü bilecektir. Derleyici CRTP uygulamak derleme zamanında karar hangi !!! Bu harika bir performans!

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

Bunu tanımlayarak yapamaz virtual void write(const char* str) const = 0;mısın? Adil olmakla birlikte, bu teknik writebaşka işler yaparken süper yararlı görünüyor .
atlex2

26
Saf bir sanal yöntem kullanarak, mirasın derleme zamanı yerine çalışma zamanında çözülmesini sağlarsınız. CRTP bunu derleme zamanında çözmek için kullanılır, böylece yürütme daha hızlı olur.
GutiMac

1
Soyut bir Yazar bekleyen düz bir işlev yapmaya çalışın: bunu yapamazsınız çünkü hiçbir yerde Writer adında bir sınıf yoktur, yani polimorfizminiz tam olarak nerede? Bu sanal işlevlerle hiç eşdeğer değildir ve çok daha az yararlıdır.

22

CRTP, derleme zamanı polimorfizmini uygulamak için bir tekniktir. İşte çok basit bir örnek. Aşağıdaki örnekte, sınıf arabirimi ProcessFoo()ile çalışıyor Baseve sanal yöntemlerle yapmayı amaçladığınız Base::Footüretilmiş nesnenin foo()yöntemini çağırıyor .

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

Çıktı:

derived foo
AnotherDerived foo

1
Bu örnekte, hiçbir Sınıflandırılmış uygulanmamışsa Base sınıfında varsayılan bir foo () uygulamasının bir örneğini eklemek de buna değer olabilir. AKA Base'deki foo'yu başka bir isimle değiştirir (örn. Caller ()), Base'ye cout'un "Base" ine yeni bir foo () işlevi ekler. Daha sonra ProcessFoo
wizurd

@wizurd Bu örnek daha çok saf bir sanal temel sınıf işlevini göstermek içindir, yani foo()türetilmiş sınıf tarafından uygulanan zorunluluğudur .
blueskin

3
Bu benim en sevdiğim cevap, çünkü bu desenin ProcessFoo()işlevle neden yararlı olduğunu da gösteriyor .
Pietro

Ben bu kodun noktası alamadım, çünkü void ProcessFoo(T* b)ve olmadan Derived ve AnotherDerived aslında türetilmiş hala işe yarayacaktı. IMHO, ProcessFoo'nun bir şekilde şablonları kullanmaması daha ilginç olurdu.
Gabriel Devillers

1
@GabrielDevillers İlk olarak, templatized ProcessFoo()arabirimi uygulayan herhangi bir tür ile çalışacaktır, yani bu durumda T giriş türü denilen bir yönteme sahip olmalıdır foo(). İkincisi, ProcessFoobirden fazla tiple çalışmak için temassız hale gelmek için , muhtemelen kaçınmak istediğimiz RTTI'yi kullanacaksınız. Ayrıca, templatized sürümü arayüz derleme süresi kontrolü sağlar.
blueskin

6

Bu doğrudan bir cevap değil, CRTP'nin nasıl faydalı olabileceğinin bir örneğidir .


İyi bir somut örneği cRTP olan std::enable_shared_from_thisC ++ 11:

[Util.smartptr.enab] / 1

Bir sınıf T, aşağıdakileri enable_­shared_­from_­this<T>alan shared_­from_­thisüye işlevlerini devralmak içinshared_­ptr için örnek işaret *this.

Yani, miras std::enable_shared_from_this örneğinize erişmeden paylaşılan (veya zayıf) bir işaretçi almayı mümkün kılar (örneğin, yalnızca bildiğiniz bir üye işlevinden)*this ).

Bir vermeniz gerektiğinde yararlıdır, std::shared_ptrancak yalnızca*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));
    }
};

Bunun thisyerine doğrudan geçememenizin nedeni shared_from_this(), sahiplik mekanizmasını bozmasıdır:

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

5

Not:

CRTP, statik polimorfizmi uygulamak için kullanılabilir (ki bu dinamik polimorfizmi sever ancak sanal fonksiyon işaret tablosu olmadan).

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

Çıktı:

Derived1 method
Derived2 method

1
üzgünüm benim kötü, static_cast değişikliği halleder. Köşe davasını hataya neden olmasa bile yine de görmek istiyorsanız, buraya bakın: ideone.com/LPkktf
odinthenerd

30
Kötü örnek. Bu kod vtableCRTP kullanılmadan yapılabilir . Ne vtablegerçekten sağlayan s türetilmiş yöntemleri çağırmak temel sınıf (işaretçi ya da referansları) kullanıyor. CRTP ile nasıl yapıldığını burada göstermelisiniz.
Etherealone

17
Örneğinizde Base<>::method (), hiç çağrılmıyor, hatta hiçbir yerde polimorfizm kullanmıyorsunuz.
MikeMB

1
@Jichao, @MikeMB bireyin nota göre, aramak gerekir methodImpliçinde methodbir Baseve türetilmiş sınıfları isim methodImplyerinemethod
Ivan Kush

1
benzer method () kullanırsanız, statik olarak bağlı ve ortak temel sınıf gerekmez. Çünkü zaten temel sınıf işaretçisi veya ref yoluyla polimorfik olarak kullanamazsınız. Bu nedenle kod şöyle görünmelidir: #include <iostream> şablonu <typename T> struct Writer {void write () {static_cast <T *> (this) -> writeImpl (); }}; struct Derived1: genel yazar <Derived1> {void writeImpl () {std :: cout << "D1"; }}; struct Derived2: genel yazar <Derived2> {void writeImpl () {std :: cout << "DER2"; }};
barney
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.