รูปแบบเทมเพลตที่เกิดซ้ำ (CRTP) คืออะไร


187

ทุกคนสามารถให้คำอธิบายที่ดีCRTPกับตัวอย่างโค้ดโดยไม่ต้องอ้างอิงหนังสือหรือไม่


2
คำถามอ่าน CRTP ในดังนั้น: stackoverflow.com/questions/tagged/crtp นั่นอาจทำให้คุณมีความคิด
sbi

68
@sbi: ถ้าเขาทำอย่างนั้นเขาจะพบคำถามของเขาเอง และนั่นจะเกิดขึ้นซ้ำ ๆ :)
Craig McQueen

1
BTW ดูเหมือนว่าคำว่าฉันควรจะ "ซ้ำซากอยากรู้อยากเห็น" ฉันเข้าใจความหมายผิดหรือเปล่า?
Craig McQueen

1
Craig: ฉันคิดว่าคุณเป็น; มันเป็น "การเกิดซ้ำอย่างแปลกประหลาด" ในแง่ที่ว่าพบการครอบตัดในหลายบริบท
Gareth McCaughan

คำตอบ:


275

กล่าวโดยย่อ CRTP คือเมื่อคลาสAมีคลาสพื้นฐานซึ่งเป็นเทมเพลตเฉพาะสำหรับคลาสAนั้น เช่น

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

มันจะอยากรู้อยากเห็นที่เกิดขึ้นไม่ได้หรือไม่ :)

ทีนี้นี่ให้อะไรคุณ สิ่งนี้ทำให้Xแม่แบบความสามารถในการเป็นคลาสพื้นฐานสำหรับความเชี่ยวชาญ

ตัวอย่างเช่นคุณสามารถสร้างคลาส singleton ทั่วไป (เวอร์ชันที่ง่ายขึ้น) เช่นนี้

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;

ทีนี้เพื่อที่จะทำให้คลาสAเป็นอะไรที่คุณควรทำ

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

เห็นไหม เทมเพลตซิงเกิลสมมติว่ามีความเชี่ยวชาญสำหรับประเภทใดก็ตามที่Xจะได้รับสืบทอดsingleton<X>และดังนั้นจึงจะสามารถเข้าถึงสมาชิก (สาธารณะป้องกัน) สมาชิกรวมถึงGetInstance! มีประโยชน์อื่น ๆ ของ CRTP ตัวอย่างเช่นหากคุณต้องการนับอินสแตนซ์ทั้งหมดที่มีอยู่ในชั้นเรียนของคุณ แต่ต้องการสรุปแคปต์ตรรกะนี้ในเทมเพลตที่แยกต่างหาก (แนวคิดสำหรับคลาสคอนกรีตค่อนข้างง่าย - มีตัวแปรแบบคงที่ ) ลองทำแบบฝึกหัด!

อีกตัวอย่างที่มีประโยชน์สำหรับ Boost (ฉันไม่แน่ใจว่าพวกเขาใช้งานอย่างไร แต่ CRTP ก็จะทำเช่นเดียวกัน) ลองนึกภาพคุณต้องการให้ผู้ให้บริการเฉพาะ<สำหรับคลาสของคุณ แต่จะดำเนินการให้โดยอัตโนมัติ==สำหรับพวกเขา!

คุณสามารถทำได้เช่นนี้

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

ตอนนี้คุณสามารถใช้มันได้เช่นนี้

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

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

ตอนนี้คุณยังไม่ได้ให้โอเปอเรเตอร์อย่างชัดเจน==เพื่อAppleอะไร แต่คุณมีมัน! คุณสามารถเขียน

int main()
{
    Apple a1;
    Apple a2; 

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

ซึ่งอาจจะดูเหมือนว่าคุณจะเขียนน้อยถ้าคุณเพียงแค่เขียนประกอบการ==สำหรับAppleแต่คิดว่าEqualityแม่แบบจะให้ไม่เพียง==แต่>, >=, <=ฯลฯ และคุณสามารถใช้คำนิยามเหล่านี้สำหรับหลาย ๆชั้นนำรหัส!

CRTP เป็นสิ่งมหัศจรรย์ :) HTH


61
โพสต์นี้ไม่สนับสนุน singleton เป็นรูปแบบการเขียนโปรแกรมที่ดีเพียงใช้มันเป็นภาพประกอบที่สามารถเข้าใจได้โดยทั่วไป the-1 ไม่ได้รับการรับรอง
John Dibling

3
@ อาร์เมน: คำตอบอธิบาย CRTP ในแบบที่สามารถเข้าใจได้อย่างชัดเจนมันเป็นคำตอบที่ดีขอบคุณสำหรับคำตอบที่ดี
Alok บันทึก

1
@Armen: ขอบคุณสำหรับคำอธิบายที่ดีนี้ ฉันไม่เคยได้รับ CRTP มาก่อน แต่ตัวอย่างความเท่าเทียมได้ให้ความกระจ่าง! +1
Paul

1
อีกตัวอย่างหนึ่งของการใช้ CRTP คือเมื่อคุณต้องการคลาสที่ไม่สามารถคัดลอกได้: template <class T> class NonCopyable {protected: NonCopyable () {} ~ NonCopyable () {} ส่วนตัว: NonCopyable (const NonCopyable &); NonCopyable & โอเปอเรเตอร์ = (const NonCopyable &); }; จากนั้นคุณใช้ noncopyable ดังนี้: คลาส Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren

2
@Puppy: ซิงเกิลไม่น่ากลัว มันถูกใช้งานมากเกินไปโดยโปรแกรมเมอร์ต่ำกว่าค่าเฉลี่ยเมื่อวิธีการอื่นจะเหมาะสมกว่า แต่การใช้งานส่วนใหญ่นั้นแย่มากไม่ได้ทำให้รูปแบบแย่ลง มีหลายกรณีที่ซิงเกิลตัวเลือกที่ดีที่สุดคือแม้ว่าจะหายาก
Kaiserludi

47

ที่นี่คุณสามารถดูตัวอย่างที่ดี หากคุณใช้วิธีเสมือนโปรแกรมจะรู้ว่าสิ่งใดที่รันในรันไทม์ การใช้งาน CRTP คอมไพเลอร์เป็นตัวกำหนดเวลาในการรวบรวม !!! นี่คือประสิทธิภาพที่ยอดเยี่ยม!

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

คุณไม่สามารถทำได้โดยการกำหนดvirtual void write(const char* str) const = 0;? แม้ว่าจะเป็นธรรมเทคนิคนี้ดูเหมือนจะมีประโยชน์มากเมื่อwriteทำงานอื่น ๆ
atlex2

26
ใช้วิธีเสมือนจริงที่คุณกำลังแก้ไขการสืบทอดในรันไทม์แทนเวลารวบรวม CRTP ใช้เพื่อแก้ปัญหานี้ในเวลาคอมไพล์ดังนั้นการดำเนินการจะเร็วขึ้น
GutiMac

1
ลองสร้างฟังก์ชั่นธรรมดาที่คาดว่าจะมีนักเขียนแบบนามธรรม: คุณไม่สามารถทำได้เพราะไม่มีคลาสชื่อ Writer ที่ใดดังนั้นความแตกต่างของคุณอยู่ที่ไหน สิ่งนี้ไม่เทียบเท่ากับฟังก์ชั่นเสมือนจริงทั้งหมดและมีประโยชน์น้อยกว่ามาก

22

CRTP เป็นเทคนิคในการใช้ polymorphism แบบรวบรวมเวลา นี่เป็นตัวอย่างง่ายๆ ในตัวอย่างด้านล่างProcessFoo()ทำงานร่วมกับBaseคลาสอินเตอร์เฟสและBase::Fooเรียกใช้foo()เมธอดของวัตถุที่ได้รับซึ่งเป็นสิ่งที่คุณมุ่งหวังที่จะทำด้วยวิธีเสมือน

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

เอาท์พุท:

derived foo
AnotherDerived foo

1
มันอาจจะคุ้มค่าในตัวอย่างนี้เพื่อเพิ่มตัวอย่างของวิธีการใช้ foo เริ่มต้น () ในคลาสฐานที่จะถูกเรียกถ้าไม่มี Derived ได้นำไปใช้ AKA เปลี่ยน foo ในฐานเป็นชื่ออื่น (เช่นผู้เรียก ()), เพิ่มฟังก์ชั่นใหม่ foo () ไปยังฐานที่ "ฐาน" ของ cout จากนั้นโทรไปที่ caller () ด้านในของ ProcessFoo
wizurd

@wizurd ตัวอย่างนี้มีมากขึ้นเพื่อแสดงให้เห็นถึงฟังก์ชั่นคลาสฐานเสมือนบริสุทธิ์เช่นเราบังคับใช้ที่foo()ดำเนินการโดยระดับที่ได้รับ
blueskin

3
นี่เป็นคำตอบที่ฉันโปรดปรานเพราะมันยังแสดงให้เห็นว่าทำไมรูปแบบนี้จึงมีประโยชน์กับProcessFoo()ฟังก์ชั่น
เปียโตร

ฉันไม่ได้รับจุดของรหัสนี้เพราะมีvoid ProcessFoo(T* b)และไม่มีการได้รับมาและ AnotherDerived จริงมามันจะยังคงทำงาน IMHO มันน่าสนใจกว่าถ้า ProcessFoo ไม่ได้ใช้ประโยชน์จากเทมเพลตอย่างใด
Gabriel Devillers

1
@GabrielDevillers ประการแรก templatized ProcessFoo()จะทำงานร่วมกับประเภทใด ๆ ที่ดำเนินการอินเตอร์เฟซเช่นในกรณีนี้การป้อนข้อมูลประเภท T foo()ควรจะมีวิธีการที่เรียกว่า ประการที่สองเพื่อให้ผู้ที่ไม่ได้รับ templatized ProcessFooสามารถทำงานกับหลายประเภทคุณอาจท้ายด้วย RTTI ซึ่งเป็นสิ่งที่เราต้องการหลีกเลี่ยง ยิ่งกว่านั้นเวอร์ชันเทมเพลตจะให้คุณตรวจสอบเวลาการคอมไพล์บนอินเทอร์เฟซ
blueskin

6

นี่ไม่ใช่คำตอบโดยตรง แต่เป็นตัวอย่างว่าCRTPมีประโยชน์อย่างไร


ตัวอย่างที่เป็นรูปธรรมที่ดีของCRTPคือstd::enable_shared_from_thisจาก C ++ 11:

[util.smartptr.enab] / 1

ชั้นTสามารถสืบทอดมาจากenable_­shared_­from_­this<T>การสืบทอดshared_­from_­thisการทำงานของสมาชิกที่ได้รับการชี้เช่นการshared_­ptr*this

นั่นคือการสืบทอดจากstd::enable_shared_from_thisทำให้เป็นไปได้ที่จะได้รับตัวชี้ (หรืออ่อน) ที่แชร์กับอินสแตนซ์ของคุณโดยไม่ต้องเข้าถึง (เช่นจากฟังก์ชั่นสมาชิกที่คุณรู้เท่านั้น*this)

มันมีประโยชน์เมื่อคุณต้องการให้std::shared_ptrแต่คุณเท่านั้นที่สามารถเข้าถึง*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));
    }
};

เหตุผลที่คุณไม่สามารถส่งthisโดยตรงได้โดยตรงแทนที่จะshared_from_this()เป็นเพราะกลไกการเป็นเจ้าของ:

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

เช่นเดียวกับหมายเหตุ:

CRTP สามารถนำมาใช้ในการสร้าง polymorphism แบบคงที่ (เช่น polymorphism แบบไดนามิก แต่ไม่มีตารางตัวชี้ฟังก์ชันเสมือน)

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

ผลลัพธ์จะเป็น:

Derived1 method
Derived2 method

1
ขออภัย static_cast ที่ไม่ดีของฉันดูแลการเปลี่ยนแปลง หากคุณต้องการดูตัวเรือน
odinthenerd

30
ตัวอย่างที่ไม่ดี รหัสนี้สามารถทำได้โดยไม่vtableต้อง s โดยไม่ต้องใช้ CRTP สิ่งที่vtableจัดเตรียมอย่างแท้จริงคือการใช้คลาสฐาน (ตัวชี้หรือการอ้างอิง) เพื่อเรียกเมธอดที่ได้รับ คุณควรแสดงให้เห็นว่า CRTP ทำอะไรที่นี่
Etherealone

17
ในตัวอย่างของคุณBase<>::method ()ไม่มีการเรียกแม้แต่คุณไม่ใช้ความหลากหลายในทุกที่
MikeMB

1
@Jichao ตามบันทึก @MikeMB ของคุณควรจะเรียกmethodImplในmethodของBaseและในชั้นเรียนมาชื่อmethodImplแทนmethod
อีวานเทือกเขาฮินดูกูช

1
ถ้าคุณใช้วิธีการที่คล้ายกัน () แล้วมันจะถูกผูกไว้แบบคงที่และคุณไม่ต้องการคลาสพื้นฐานทั่วไป เพราะอย่างไรก็ตามคุณไม่สามารถใช้มันผ่าน polymorphically ผ่านตัวชี้คลาสพื้นฐานหรืออ้างอิง ดังนั้นรหัสควรมีลักษณะดังนี้: #include <iostream> template <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"; }};
บาร์นีย์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.