The first technical sharing in the ShengquGames

I mainly explained the use of CRTP in C++.

The CRTP is an abbreviation for curiously recurring template pattern.

C++’s template functionality gives C++ a usage similar to duck type. Duck type is a style of dynamic type in programming. In this style, the effective semantics of an object is not determined by deriving from a specific class or implementing a specific interface, but by “the collection of current methods and properties”.

We can use the duck type to achieve the following requirements:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A:
def interface1(self):
print("interface1 in class A")
def interface2(self):
print("interface2 in class A")

class B:
def interface1(self):
print("interface1 in class B")
def interface2(self):
print("interface2 in class B")

def func(args):
args.interface1()
args.interface2()

def play():
a = A()
b = B()
func(a)
func(b)

play()

Inheritance is not used here, but polymorphism is still implemented.

In C++, we can use the template to implement the duck type to achieve static polymorphism.Write like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <vector>

class Base {
public:
virtual void interface() = 0;
virtual ~Base() {}
};

template <typename DrivedType>
class TemplateBase: public Base {
public:
DrivedType* Self() {
return static_cast<DrivedType*> (this);
}
void interface() override {
Self()->func();
}
};

class A: public TemplateBase<A> {
public:
void func() {
std::cout << "A" << std::endl;
}
};

class B: public TemplateBase<B> {
public:
void func() {
std::cout << "B" << std::endl;
}
};

int main () {
std::vector<Base*> vec;
vec.push_back(new A());
vec.push_back(new B());
for (const auto& iter : vec) {
iter->interface();
}
vec.clear();
std::vector<Base*>(vec).swap(vec);
return 0;
}

In the above code, we can not write the base class, but directly write the template function to use the instantiated template class as a template parameter, but this is just a duck type. If we want to construct a vector, then we can’t implement it. Because the specialization template class is not the same type, you must use a template class to inherit the base class, and then use the base class pointer as the template parameter of the vector to implement static polymorphism.
There is also a place to pay attention to, we use static_cast in the template class, but our derived class inherits the template class specialized by its own type, so the use of static_cast is safe here.

Another role of CRTP is to define abstract methods and reduce redundancy.

If we need to do a math library, like the code below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Vector2 {
float x;
float y;
float z;

Vector2() = default;

Vector2(float _x, float _y, float _z);

inline Vector2& operator+=(const Vector2& rhs);
inline Vector2& operator-=(const Vector2& rhs);
//....
};

inline Vector2 operator+(const Vector3& lhs, const Vector2& rhs);
inline Vector2 operator-(const Vector3& lhs, const Vector2& rhs);
//....

//Vec3
class Vector3 {
float x;
float y;

Vector3() = default;

Vector3(float _x, float _y);

inline Vector3& operator+=(const Vector3& rhs);
inline Vector3& operator-=(const Vector3& rhs);
//....
};

We can notice that we need to write a lot of logical similar code, we can abstract it into the template class, and then use the subclass-specific methods to implement the abstract method. Like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
template<typename T>
class VectorBase {
T& underlying() { return static_cast<T&>(*this); }
T const& underlying() const { return static_cast<T const&>(*this); }

inline T& operator+=(const T& rhs) {
this->underlying() = this->underlying() + rhs;
return this->underlying();
}

inline T& operator-=(const T& rhs) {
this->underlying() = this->underlying() - rhs;
return this->underlying();
}

//.....
};

class Vector3 : public VectorBase<Vector3> {
float x;
float y;
float z;

Vector3() = default;

Vector3(float _x, float _y, float _z)
: x{_x}, y{_y}, z{_z} { }
};

inline Vector3 operator+(const Vector3& lhs, const Vector3& rhs) {
Vector3 result;
result.x = lhs.x + rhs.x;
result.y = lhs.y + rhs.y;
result.z = lhs.z + rhs.z;
return result;
}

inline Vector3 operator-(const Vector3& lhs, const Vector3& rhs) {
Vector3 result;
result.x = lhs.x - rhs.x;
result.y = lhs.y - rhs.y;
result.z = lhs.z - rhs.z;
return result;
}
//......

int main() {
Vector3 v0(6.0f, 5.0f, 4.0f);
Vector3 v2(4.0f, 5.0f, 6.0f);

v0 += v2;
v0 -= v2;
return 0;
}

I learned from the cpp reference that the usage of std::enable_shared_from_this is also related to CRTP, but I don’t know it yet, so mark it and write it later…

script>