Mẫu mẫu lặp lại tò mò (CRTP) là gì?


187

Không cần tham khảo một cuốn sách, bất cứ ai cũng có thể vui lòng cung cấp một lời giải thích tốt cho CRTPmột ví dụ mã?


2
Đọc câu hỏi CRTP trên SO: stackoverflow.com/questions/tagged/crtp . Điều đó có thể cho bạn một số ý tưởng.
sbi

68
@sbi: Nếu anh ấy làm điều đó, anh ấy sẽ tìm thấy câu hỏi của riêng mình. Và điều đó sẽ được tò mò tái diễn. :)
Craig McQueen

1
Đối với tôi, BTW có vẻ như là "đệ quy tò mò". Tôi có hiểu nhầm ý nghĩa không?
Craig McQueen

1
Craig: Tôi nghĩ bạn là; đó là "lặp đi lặp lại một cách tò mò" theo nghĩa là nó được tìm thấy mọc lên trong nhiều bối cảnh.
Gareth McCaughan

Câu trả lời:


275

Nói tóm lại, CRTP là khi một lớp Acó một lớp cơ sở là một chuyên môn mẫu cho Achính lớp đó. Ví dụ

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

Đó tò mò định kỳ, phải không? :)

Bây giờ, những gì nó cung cấp cho bạn? Điều này thực sự mang lại cho Xmẫu khả năng trở thành một lớp cơ sở cho các chuyên ngành của nó.

Ví dụ: bạn có thể tạo một lớp singleton chung (phiên bản đơn giản hóa) như thế này

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;

Bây giờ, để biến một lớp tùy ý Athành một singleton, bạn nên làm điều này

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

Bạn thấy đó? Mẫu singleton giả định rằng chuyên môn hóa của nó cho bất kỳ loại nào Xsẽ được kế thừa từ singleton<X>đó và do đó sẽ có tất cả các thành viên (công khai, được bảo vệ) có thể truy cập được, bao gồm cả GetInstance! Có những cách sử dụng hữu ích khác của CRTP. Ví dụ: nếu bạn muốn đếm tất cả các cá thể hiện đang tồn tại cho lớp của bạn, nhưng muốn gói gọn logic này trong một mẫu riêng (ý tưởng cho một lớp cụ thể khá đơn giản - có một biến tĩnh, tăng dần trong các hàm, giảm dần trong các hàm ). Hãy cố gắng làm nó như một bài tập!

Một ví dụ hữu ích khác, đối với Boost (Tôi không chắc họ đã triển khai nó như thế nào, nhưng CRTP cũng sẽ làm như vậy). Hãy tưởng tượng bạn muốn chỉ cung cấp toán tử <cho các lớp của mình nhưng tự động vận hành ==cho chúng!

bạn có thể làm như thế này:

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

Bây giờ bạn có thể sử dụng nó như thế này

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

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

Bây giờ, bạn đã không cung cấp toán tử rõ ràng ==cho Apple? Nhưng bạn có nó! Bạn có thể viết

int main()
{
    Apple a1;
    Apple a2; 

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

Điều này có vẻ như bạn sẽ viết ít hơn nếu bạn chỉ viết toán tử ==cho Apple, nhưng hãy tưởng tượng rằng Equalitymẫu sẽ cung cấp không chỉ , ==mà , v.v. Và bạn có thể sử dụng các định nghĩa này cho nhiều lớp, sử dụng lại mã!>>=<=

CRTP là một điều tuyệt vời :) HTH


61
Bài này không ủng hộ singleton như một pattern.it tốt lập trình chỉ đơn giản là sử dụng nó như là một minh chứng rằng có thể thường understood.imo 1 the-là không có cơ sở
John Dibling

3
@Armen: Câu trả lời giải thích CRTP theo cách có thể hiểu rõ ràng, đó là một câu trả lời hay, cảm ơn vì một câu trả lời hay như vậy.
Alok Lưu

1
@Armen: cảm ơn vì lời giải thích tuyệt vời này. Tôi đã từng bị CRTP trước đây, nhưng ví dụ về sự bình đẳng đã được chiếu sáng! +1
Paul

1
Một ví dụ khác về việc sử dụng CRTP là khi bạn cần một lớp không thể sao chép: template <class T> class NonCopyable {bảo vệ: NonCopyable () {} ~ NonCopyable () {} private: NonCopyable (const NonCopyable &); NonCopyable & toán tử = (const NonCopyable &); }; Sau đó, bạn sử dụng không thể sao chép như dưới đây: class Mutex: private NonCopyable <Mutex> {public: void Lock () {} void UnLock () {}};
Viren

2
@Puppy: Singleton không tệ. Nó được sử dụng quá mức bởi các lập trình viên dưới mức trung bình khi các cách tiếp cận khác sẽ phù hợp hơn, nhưng hầu hết các cách sử dụng của nó là khủng khiếp không làm cho mô hình trở nên khủng khiếp. Có những trường hợp singleton là lựa chọn tốt nhất, mặc dù những trường hợp này rất hiếm.
Kaiserludi

47

Ở đây bạn có thể thấy một ví dụ tuyệt vời. Nếu bạn sử dụng phương thức ảo, chương trình sẽ biết những gì thực thi trong thời gian chạy. Việc thực hiện CRTP trình biên dịch sẽ quyết định thời gian biên dịch !!! Đây là một màn trình diễn tuyệt vời!

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

Bạn không thể làm điều này bằng cách xác định virtual void write(const char* str) const = 0;? Mặc dù công bằng, kỹ thuật này có vẻ siêu hữu ích khi writethực hiện các công việc khác.
atlex2

24
Sử dụng một phương thức ảo thuần túy, bạn đang giải quyết sự kế thừa trong thời gian chạy thay vì biên dịch thời gian. CRTP được sử dụng để giải quyết điều này trong thời gian biên dịch nên việc thực thi sẽ nhanh hơn.
GutiMac

1
Hãy thử tạo một hàm đơn giản mong đợi một Nhà văn trừu tượng: bạn không thể làm điều đó bởi vì không có lớp nào có tên là Nhà văn ở bất cứ đâu, vậy tính đa hình của bạn chính xác ở đâu? Điều này hoàn toàn không tương đương với các chức năng ảo và nó ít hữu ích hơn nhiều.

22

CRTP là một kỹ thuật để thực hiện đa hình thời gian biên dịch. Đây là một ví dụ rất đơn giản. Trong ví dụ dưới đây, ProcessFoo()đang làm việc với Basegiao diện lớp và Base::Foogọi foo()phương thức của đối tượng dẫn xuất , đây là điều bạn nhắm đến để làm với các phương thức ảo.

http://coliru.stacked-crooking.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;
}

Đầu ra:

derived foo
AnotherDerived foo

1
Nó cũng có thể có giá trị trong ví dụ này để thêm một ví dụ về cách triển khai foo () mặc định trong lớp Cơ sở sẽ được gọi nếu không có Derogen đã triển khai nó. AKA thay đổi foo trong Base thành một số tên khác (ví dụ: người gọi ()), thêm chức năng mới foo () vào Base mà cout là "Base". Sau đó gọi người gọi () bên trong ProcessFoo
wizurd

@wizurd Ví dụ này là nhiều hơn để minh họa một hàm lớp cơ sở ảo thuần túy tức là chúng ta thi hành foo()được thực hiện bởi lớp dẫn xuất.
blueskin

3
Đây là câu trả lời yêu thích của tôi, vì nó cũng cho thấy tại sao mẫu này hữu ích với ProcessFoo()hàm.
Pietro

Tôi không hiểu ý nghĩa của mã này, bởi vì có void ProcessFoo(T* b)và không có Derogen và AnotherDeriving thực sự có nguồn gốc, nó vẫn hoạt động. IMHO sẽ thú vị hơn nếu ProcessFoo không sử dụng các mẫu nào đó.
Gabriel Devillers

1
@GabrielDevillers Trước tiên, templatized ProcessFoo()sẽ hoạt động với bất kỳ loại nào thực hiện giao diện, tức là trong trường hợp này, kiểu đầu vào T nên có một phương thức được gọi foo(). Thứ hai, để làm cho một ứng dụng không có templatized ProcessFoohoạt động với nhiều loại, bạn có thể sẽ sử dụng RTTI, đây là điều chúng tôi muốn tránh. Hơn nữa, phiên bản templatized cung cấp cho bạn kiểm tra thời gian biên dịch trên giao diện.
blueskin

6

Đây không phải là một câu trả lời trực tiếp, mà là một ví dụ về cách CRTP có thể hữu ích.


Một ví dụ cụ thể về CRTPstd::enable_shared_from_thistừ C ++ 11:

[produc.smartptr.enab] / 1

Một lớp Tcó thể kế thừa từ enable_­shared_­from_­this<T>để kế thừa các shared_­from_­thishàm thành viên có được một shared_­ptrthể hiện trỏ tới *this.

Đó là, kế thừa từ std::enable_shared_from_thislàm cho nó có thể đưa một con trỏ được chia sẻ (hoặc yếu) vào thể hiện của bạn mà không cần truy cập vào nó (ví dụ từ một hàm thành viên mà bạn chỉ biết về *this).

Nó hữu ích khi bạn cần cung cấp std::shared_ptrnhưng bạn chỉ có quyền truy cập vào *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));
    }
};

Lý do bạn không thể vượt qua thistrực tiếp thay vì shared_from_this()nó sẽ phá vỡ cơ chế sở hữu:

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

Cũng như ghi chú:

CRTP có thể được sử dụng để thực hiện đa hình tĩnh (giống như đa hình động nhưng không có bảng con trỏ hàm ảo).

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

Đầu ra sẽ là:

Derived1 method
Derived2 method

1
xin lỗi, xấu của tôi, static_cast quan tâm đến sự thay đổi. Nếu bạn vẫn muốn xem trường hợp góc dù nó không gây ra lỗi, hãy xem tại đây: ideone.com/LPkktf
odinthenerd

30
Ví dụ xấu. Mã này có thể được thực hiện mà không có vtables mà không sử dụng CRTP. Những gì vtablethực sự cung cấp là sử dụng lớp cơ sở (con trỏ hoặc tham chiếu) để gọi các phương thức dẫn xuất. Bạn nên chỉ ra cách nó được thực hiện với CRTP tại đây.
Etherealone

17
Trong ví dụ của bạn, Base<>::method ()thậm chí không được gọi, bạn cũng không sử dụng đa hình ở bất cứ đâu.
MikeMB

1
@Jichao, theo ghi chú của @MikeMB, bạn nên gọi methodImpltên methodcủa Basevà trong các lớp dẫn xuất methodImplthay vìmethod
Ivan Kush

1
nếu bạn sử dụng phương thức tương tự () thì nó bị ràng buộc tĩnh và bạn không cần lớp cơ sở chung. Bởi vì dù sao bạn cũng không thể sử dụng nó một cách đa hình thông qua con trỏ lớp cơ sở hoặc ref. Vì vậy, mã sẽ trông như thế này: #include <iostream> template <typename T> struct Writer {void write () {static_cast <T *> (this) -> writeImpl (); }}; struct Derured1: Nhà văn công khai <Derured1> {void writeImpl () {std :: cout << "D1"; }}; struct Derured2: Nhà văn công khai <Derured2> {void writeImpl () {std :: cout << "DER2"; }};
barney
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.