DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Dynamic File Upload Component in Salesforce LWC
  • Prototype Pattern in JavaScript
  • Building an AWS Integration for Salesforce Image Handling
  • Cutting-Edge Object Detection for Autonomous Vehicles: Advanced Transformers and Multi-Sensor Fusion

Trending

  • AWS to Azure Migration: A Cloudy Journey of Challenges and Triumphs
  • Start Coding With Google Cloud Workstations
  • Agentic AI for Automated Application Security and Vulnerability Management
  • MySQL to PostgreSQL Database Migration: A Practical Case Study
  1. DZone
  2. Coding
  3. Languages
  4. Applying Curiously Recurring Template Pattern in C++

Applying Curiously Recurring Template Pattern in C++

There is various material effectively accessible for "How" and "What" on CRTP. So, I will address the "Where" part that CRTP Applicability.

By 
Vishal Chovatiya user avatar
Vishal Chovatiya
·
Updated Aug. 03, 20 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
10.8K Views

Join the DZone community and get the full member experience.

Join For Free

Curiously Recurring Template Pattern(CRTP) in C++ is definitely a powerful technique and a static alternative to virtual functions. But at the same time, learning it may seem a bit weird at first. If you are like me who struggled to grasp anything in one go. Then this article might help you to provide a thought process on where CRTP fits in day-to-day coding. And, if you are an Embedded Programmer, you may run into CRTP more often. Although, std::variant + std::visit will also help but 90% of the compilers for embedded processors are either not up to date with standard or dumb.

There is various material effectively accessible for "How" and "What" on CRTP. So, I will address the "Where" part that CRTP Applicability.

CRTP and Static Polymorphism In C++

C++
 




x
23


 
1
template<typename specific_animal>
2
struct animal {
3
    void who() { static_cast<specific_animal*>(this)->who(); }
4
};
5

           
6
struct dog : animal<dog> {
7
    void who() { cout << "dog" << endl; }
8
};
9

           
10
struct cat : animal<cat> {
11
    void who() { cout << "cat" << endl; }
12
};
13

           
14
template<typename specific_animal>
15
void who_am_i(animal<specific_animal> andanimal) {
16
    animal.who();
17
}
18

           
19
cat c;
20
who_am_i(c); // prints `cat`
21

           
22
dog d;
23
who_am_i(d); // prints `dog`



  • Curiously Recurring Template Pattern widely employed for static polymorphism without bearing the cost of a virtual dispatch mechanism. Consider the above code, we haven't used virtual keyword and still achieved the functionality of polymorphism.
  • How it works is not the topic of this article. So, I am leaving it to you to figure out.

Limiting Object Count With CRTP

  • There are times when you have to manage the critical resource with single or predefined object count. And we have Singleton and Monotone Design Patterns for this. But this works as long as your object counts are smaller in number.
  • When you want to limit the arbitrary type to be limited with an arbitrary number of instances. CRTP will come to rescue:
C++
 




xxxxxxxxxx
1
28


 
1
template <class ToBeLimited, uint32_t maxInstance>
2
struct LimitNoOfInstances {
3
    static atomic<uint32_t> cnt;
4

           
5
    LimitNoOfInstances() {
6
        if (cnt >= maxInstance)
7
            throw logic_error{"Too Many Instances"};
8
        ++cnt;
9
    }
10
    ~LimitNoOfInstances() { --cnt; }
11
}; // Copy, move & other sanity checks to be complete
12

           
13
struct One : LimitNoOfInstances<One, 1> {};
14
struct Two : LimitNoOfInstances<Two, 2> {};
15

           
16
template <class T, uint32_t maxNoOfInstace>
17
atomic<uint32_t> LimitNoOfInstances<T, maxNoOfInstace>::cnt(0);
18

           
19

           
20
void use_case() {
21
    Two _2_0, _2_1;
22

           
23
    try {
24
        One _1_0, _1_1;
25
    } catch (exception &e) {
26
        cout << e.what() << endl;
27
    }
28
}



  • You might be wondering that what is the point of the template parameter ToBeLimited, if it isn't used. In that case, you should have brush up your C++ Template fundamentals or use cppinsights.io. As it isn't useless.

CRTP to Avoid Code Duplication

  • Let say you have a set of containers that support the functions begin() and end(). But, the standard library's requirements for containers require more functionalities like front(), back(), size(), etc.
  • We can design such functionalities with a CRTP base class that provides common utilities solely based on derived class member function i.e. begin() and end() in our cases:
C++
 




xxxxxxxxxx
1
11


 
1
template <typename T>
2
class Container {
3
    T &actual() { return *static_cast<T *>(this); }
4
    T const &actual() const { return *static_cast<T const *>(this); }
5

           
6
public:
7
    decltype(auto) front() { return *actual().begin(); }
8
    decltype(auto) back() { return *std::prev(actual().end()); }
9
    decltype(auto) size() const { return std::distance(actual().begin(), actual().end()); }
10
    decltype(auto) operator[](size_t i) { return *std::next(actual().begin(), i); }
11
};



  • The above class provides the functions front(), back(), size() and operator[ ] for any subclass that has begin() and end().
  • For example, subclass could be a simple dynamically allocated array as:
C++
 




xxxxxxxxxx
1
19


 
1
template <typename T>
2
class DynArray : public Container<DynArray<T>> {
3
    size_t m_size;
4
    unique_ptr<T[]> m_data;
5

           
6
  public:
7
    DynArray(size_t s) : m_size{s}, m_data{make_unique<T[]>(s)} {}
8

           
9
    T *begin() { return m_data.get(); }
10
    const T *begin() const { return m_data.get(); }
11

           
12
    T *end() { return m_data.get() + m_size; }
13
    const T *end() const { return m_data.get() + m_size; }
14
};
15

           
16
DynArray<int> arr(10);
17
arr.front() = 2;
18
arr[2]        = 5;
19
asssert(arr.size() == 10);



Modern C++ Composite Design Pattern Leveraging CRTP

  • Composite Design Pattern states that we should treat the group of objects in the same manner as a single object. And to implement such a pattern we can leverage the CRTP.
  • For example, as a part of machine learning, we have to deal with Neuron which for simplicity defined as:
C++
 




xxxxxxxxxx
1
34


 
1
struct Neuron {
2
    vector<Neuron*>     in, out;    // Stores the input-output connnections to other Neurons
3
    uint32_t            id;
4

           
5
    Neuron() {
6
        static int id = 1;
7
        this->id = id++;
8
    }
9

           
10
    void connect_to(Neuron &other) {
11
        out.push_back(&other);
12
        other.in.push_back(this);
13
    }
14

           
15
    friend ostream &operator<<(ostream &os, const Neuron &obj) {
16
        for (Neuron *n : obj.in)
17
            os << n->id << "\t-->\t[" << obj.id << "]" << endl;
18

           
19
        for (Neuron *n : obj.out)
20
            os << "[" << obj.id << "]\t-->\t" << n->id << endl;
21

           
22
        return os;
23
    }
24
};
25

           
26
Neuron n1, n2;
27
n1.connect_to(n2);
28
cout << n1 << n2 << endl;
29

           
30
/* Output
31
[1]    -->    2
32
1    -->    [2]
33
*/
34

           



And there is also a NeuronLayer i.e. collection of Neuron which for simplicity defined as:
C++
 




xxxxxxxxxx
1
12


 
1
struct NeuronLayer : vector<Neuron> {
2
    NeuronLayer(int count) {
3
        while (count --> 0)
4
            emplace_back(Neuron{});
5
    }
6

           
7
    friend ostream &operator<<(ostream &os, NeuronLayer &obj) {
8
        for (auto &n : obj)
9
            os << n;
10
        return os;
11
    }
12
};


  • Now, if you want to connect the Neuron with NeuronLayer and vice-versa. You're going to have a total of four different functions as follows:
C++
 




xxxxxxxxxx
1


 
1
Neuron::connect_to(Neuron&)
2
Neuron::connect_to(NeuronLayer&)
3

           
4
NeuronLayer::connect_to(NeuronLayer&)
5
NeuronLayer::connect_to(Neuron&)



  • You see this is state-space explosion(permutation in layman terms) problem and it's not good. Because we want a single function that enumerable both the layer as well as individual neurons. CRTP comes handy here as:
C++
 




xxxxxxxxxx
1
89


 
1
template <typename Self>
2
struct SomeNeurons {
3
    template <typename T>
4
    void connect_to(T &other);
5
};
6

           
7
struct Neuron : SomeNeurons<Neuron> {
8
    vector<Neuron*>     in, out;
9
    uint32_t            id;
10

           
11
    Neuron() {
12
        static int id = 1;
13
        this->id = id++;
14
    }
15

           
16
    Neuron* begin() { return this; }
17
    Neuron* end() { return this + 1; }
18
};
19

           
20
struct NeuronLayer : vector<Neuron>, SomeNeurons<NeuronLayer> {
21
    NeuronLayer(int count) {
22
        while (count-- > 0)
23
            emplace_back(Neuron{});
24
    }
25
};
26

           
27
/* ----------------------------------------------------------------------- */
28
template <typename Self>
29
template <typename T>
30
void SomeNeurons<Self>::connect_to(T &other) {
31
    for (Neuron &from : *static_cast<Self *>(this)) {
32
        for (Neuron &to : other) {
33
            from.out.push_back(&to);
34
            to.in.push_back(&from);
35
        }
36
    }
37
}
38
/* ----------------------------------------------------------------------- */
39

           
40
template <typename Self>
41
ostream &operator<<(ostream &os, SomeNeurons<Self> &object) {
42
    for (Neuron &obj : *static_cast<Self *>(&object)) {
43
        for (Neuron *n : obj.in)
44
            os << n->id << "\t-->\t[" << obj.id << "]" << endl;
45

           
46
        for (Neuron *n : obj.out)
47
            os << "[" << obj.id << "]\t-->\t" << n->id << endl;
48
    }
49
    return os;
50
}
51

           
52
int main() {
53
    Neuron n1, n2;
54
    NeuronLayer l1{1}, l2{2};
55

           
56
    n1.connect_to(l1); // Scenario 1: Neuron connects to Layer
57
    l2.connect_to(n2); // Scenario 2: Layer connects to Neuron
58
    l1.connect_to(l2); // Scenario 3: Layer connects to Layer
59
    n1.connect_to(n2); // Scenario 4: Neuron connects to Neuron
60

           
61
    cout << "Neuron " << n1.id << endl << n1 << endl;
62
    cout << "Neuron " << n2.id << endl << n2 << endl;
63

           
64
    cout << "Layer " << endl << l1 << endl;
65
    cout << "Layer " << endl << l2 << endl;
66

           
67
    return EXIT_SUCCESS;
68
}
69
/* Output
70
Neuron 1
71
[1]    -->    3
72
[1]    -->    2
73

           
74
Neuron 2
75
4    -->    [2]
76
5    -->    [2]
77
1    -->    [2]
78

           
79
Layer 
80
1    -->    [3]
81
[3]    -->    4
82
[3]    -->    5
83

           
84
Layer 
85
3    -->    [4]
86
[4]    -->    2
87
3    -->    [5]
88
[5]    -->    2
89
*/



  • As you can see we have covered all four different permutation scenarios using a single SomeNeurons::connect_to method. And both Neuron and NeuronLayer conforms to this interface via self templatization.

C++20 Spaceship Operator With the Help of CRTP

Problem

C++
 




xxxxxxxxxx
1
22


 
1
struct obj_type_1 {
2
    bool operator<(const value &rhs) const { return m_x < rhs.m_x; }
3
    // bool operator==(const value &rhs) const;
4
    // bool operator!=(const value &rhs) const;    
5
    // List goes on. . . . . . . . . . . . . . . . . . . .
6
private:
7
    // data members to compare
8
};
9

           
10
struct obj_type_2 {
11
    bool operator<(const value &rhs) const { return m_x < rhs.m_x; }
12
    // bool operator==(const value &rhs) const;
13
    // bool operator!=(const value &rhs) const;    
14
    // List goes on. . . . . . . . . . . . . . . . . . . .
15
private:
16
    // data members to compare
17
};
18

           
19
struct obj_type_3 { ...
20
struct obj_type_4 { ...
21
// List goes on. . . . . . . . . . . . . . . . . . . .
22

           


For each comparable objects, you need to define respective comparison operators. This is redundant because if we have an operator < , we can overload other operators based on it.
  • Thus, operator < is the only one operator having type information, other operators can be made type independent for reusability purposes.

Solution Till C++17 With CRTP

C++
 




xxxxxxxxxx
1
29


 
1
template <class derived>
2
struct compare {};
3

           
4
struct value : compare<value> {
5
    int m_x;
6
    value(int x) : m_x(x) {}
7
    bool operator < (const value &rhs) const { return m_x < rhs.m_x; }
8
};
9

           
10
template <class derived>
11
bool operator > (const compare<derived> &lhs, const compare<derived> &rhs) {
12
    // static_assert(std::is_base_of_v<compare<derived>, derived>); // Compile time safety measures
13
    return (static_cast<const derived&>(rhs) < static_cast<const derived&>(lhs));
14
}
15

           
16
/*  Same goes with other operators
17
    == :: returns !(lhs < rhs) and !(rhs < lhs)
18
    != :: returns !(lhs == rhs)
19
    >= :: returns (rhs < lhs) or (rhs == lhs)
20
    <= :: returns (lhs < rhs) or (rhs == lhs) 
21
*/
22

           
23
int main() {   
24
    value v1{5}, v2{10};
25
    cout << boolalpha << "v1 > v2: " << (v1 > v2) << '\n';
26
    return EXIT_SUCCESS;
27
}
28
// Now no need to write comparator operators for all the classes, 
29
// Write only type dependent `operator <` & inherit with `compare<T>`


C++20 Solution : Spaceship Operator

C++
 




xxxxxxxxxx
1


 
1
struct value{
2
    int m_x;
3
    value(int x) : m_x(x) {}
4
    auto operator<=>(const value &rhs) const = default;
5
};
6
// Defaulted equality comparisons
7
// More Info: https://en.cppreference.com/w/cpp/language/default_comparisons



Enabling Polymorphic Method Chaining

  • Method Chaining is a common syntax for invoking multiple methods on a single object back to back. That too, in a single statement without requiring variables to store the intermediate results. For example:
C++
 




xxxxxxxxxx
1
18


 
1
class Printer {
2
    ostream &m_stream;
3
public:
4
    Printer(ostream &s) : m_stream(s) { }
5

           
6
    Printer &print(auto &&t) {
7
        m_stream << t;
8
        return *this;
9
    }
10

           
11
    Printer &println(auto &&t) {
12
        m_stream << t << endl;
13
        return *this;
14
    }
15
};
16

           
17
Printer{cout}.println("hello").println(500);     // Method Chaining
18

           


But, when method chaining applied to an object hierarchy, things can go wrong. For example:
C++
 




xxxxxxxxxx
1
11


 
1
struct ColorPrinter : Printer {
2
    enum Color{red, blue, green};
3
    ColorPrinter(ostream &s) : Printer(s) {}
4

           
5
    ColorPrinter &SetConsoleColor(Color c) {
6
        // ...
7
        return *this;
8
    }
9
};
10

           
11
ColorPrinter(cout).print("Hello").SetConsoleColor(ColorPrinter::Color::red).println("Printer!"); // Not OK



  • Compiling above code prompt you with the following error:
C++
 




xxxxxxxxxx
1


 
1
error: 'class Printer' has no member named 'SetConsoleColor'
2

           
3
ColorPrinter(cout).print("Hello").SetConsoleColor(ColorPrinter::Color::red).println("Printer!");
4
                                  ^
5
                                  |____________ We have a 'Printer' here, not a 'ColorPrinter'



  • This happens because we "lose" the concrete class as soon as we invoke a function of the base class.
  • The CRTP can be useful to avoid such a problem and to enable Polymorphic Method Chaining.
C++
 




xxxxxxxxxx
1
31


 
1
template <typename ConcretePrinter>
2
class Printer {
3
    ostream &m_stream;
4
public:
5
    Printer(ostream &s) : m_stream(s) { }
6

           
7
    ConcretePrinter &print(auto &&t) {
8
        m_stream << t;
9
        return static_cast<ConcretePrinter &>(*this);
10
    }
11

           
12
    ConcretePrinter &println(auto &&t) {
13
        m_stream << t << endl;
14
        return static_cast<ConcretePrinter &>(*this);
15
    }
16
};
17

           
18
struct ColorPrinter : Printer<ColorPrinter> {
19
    enum Color { red, blue, green };
20
    ColorPrinter(ostream &s) : Printer(s) {}
21

           
22
    ColorPrinter &SetConsoleColor(Color c) {
23
        // ...
24
        return *this;
25
    }
26
};
27

           
28
int main() {
29
    ColorPrinter(cout).print("Hello ").SetConsoleColor(ColorPrinter::Color::red).println("Printer!");
30
    return EXIT_SUCCESS;
31
}



Enabling Polymorphic Copy Construction in C++ with CRTP

Problem

  • C++ has the support of polymorphic object destruction using it's base class's virtual destructor. But, equivalent support for creation and copying of objects is missing as С++ doesn't support virtual constructor/copy-constructors.
  • Moreover, you can't create an object unless you know its static type, because the compiler must know the amount of space it needs to allocate. For the same reason, a copy of an object also requires its type to known at compile-time.
C++
 




xxxxxxxxxx
1
11


 
1
struct animal {  virtual ~animal(){ cout << "~animal\n"; } };
2

           
3
struct dog : animal  { ~dog(){ cout << "~dog\n"; } };
4
struct cat : animal  { ~cat(){ cout << "~cat\n"; } };
5

           
6
void who_am_i(animal *who) { // not sure whether `dog` would be passed here or `cat`
7

           
8
    // How to `copy` object of the same type i.e. pointed by who?
9

           
10
    delete who; // you can delete object pointed by who
11
}



Solution 1: Dynamic Polymorphism

  • As the name suggests, we will use virtual methods to delegate the act of copying(and/or creation) of the object as below:
C++
 




xxxxxxxxxx
1
15


 
1
struct animal {
2
    virtual unique_ptr<animal> clone() = 0;
3
};
4

           
5
struct dog : animal {
6
    unique_ptr<animal> clone() override { return make_unique<dog>(*this); }
7
};
8

           
9
struct cat : animal {
10
    unique_ptr<animal> clone() override { return make_unique<cat>(*this); }
11
};
12

           
13
void who_am_i(animal *who) {
14
    auto duplicate_who = who->clone(); // `copy` object of same type i.e. pointed by who ?    
15
}



Solution 2: Static Polymorphism

  • The same thing can be accomplished with CRTP as below:
C++
 




xxxxxxxxxx
1
23


 
1
template <class specific>
2
struct animal {
3
    unique_ptr<animal> clone() {
4
        return make_unique<specific>(static_cast<specific &>(*this));
5
    }
6

           
7
protected: // Forcing animal class to be inherited
8
    animal(const animal &) = default;
9
};
10

           
11
struct dog : animal<dog> {
12
    dog(const dog &) { cout << "copied dog" << endl; }
13
};
14

           
15
struct cat : animal<cat> {
16
    cat(const cat &) { cout << "copied cat" << endl; }
17
};
18

           
19
template <class specific>
20
void who_am_i(animal<specific> *who) {
21
    auto duplicate_who = who->clone(); // `copy` object of same type i.e. pointed by who ?
22
}
23

           



Closing Words

Everything comes with its price. And CRTP is no exception. For example, if you are using CRTP with run time object creation, your code may behave weird. Moreover,

  • As the base class is templated, you can not point the derived class object with the base class pointer.
  • Also, you can not create generic container like std::vector<animal*> because animal is not a class, but a template needing specialization. A container defined as std::vector<animal<dog>*> can only store dogs, not cats. This is because each of the classes derived from the CRTP base class animal is a unique type. A common solution to this problem is to add one more layer of indirection i.e. abstract class with a virtual destructor, like the abstract_animal and inherit animal class, allowing for the creation of a std::vector<abstract_animal*>.

There is another useful application of CRTP as well. If you think I am missing any major one and have any suggestions you can always reach me here.

  References

  • Wikipedia
  • C++ Notes for Professionals Stack Overflow Documentation
  • Advanced C++ Concepts 
Template Object (computer science)

Published at DZone with permission of Vishal Chovatiya. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Dynamic File Upload Component in Salesforce LWC
  • Prototype Pattern in JavaScript
  • Building an AWS Integration for Salesforce Image Handling
  • Cutting-Edge Object Detection for Autonomous Vehicles: Advanced Transformers and Multi-Sensor Fusion

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!