In this post, I will reflect on the topic of static polymorphism in C ++, the architectural solutions built on its basis. Consider an interesting idiom -
CRTP . I will give a few examples of its use. In particular, I will consider the concept of
MixIn classes. I am writing to systematize my own knowledge, but maybe you can find something interesting for yourself.
Introduction
As you know, C ++ is a multi-paradigm language. You can write in it in a procedural style, use language constructs that support object-oriented programming, templates allow generalized programming,
STL, and new language features (
lambda, std :: function, std :: bind ), if you wish, you can write in a functional style in runtime, and template metaprogramming is a pure functional programming in
compile time .
Despite the fact that in any real large program you can most likely encounter a mixture of all these techniques, an object-oriented paradigm implemented using the concept of classes, an open interface and a closed implementation (encapsulation), inheritance, and dynamic polymorphism implemented through virtual functions, undoubtedly the most widely used.
However, dynamic polymorphism is not free. Despite the fact that the time spent on calling a virtual function is not too large, under certain circumstances, for example, a cycle that processes many polymorphic objects, the overhead of such a solution becomes noticeable compared to ordinary functions.
Static polymorphism
While dynamic polymorphism is a polymorphism of runtime and explicit interfaces, static polymorphism is a polymorphism of compile time and implicit interfaces. Let's see what this means.
Looking at the following code
')
void process(base* b) { b->prepare(); b->work(); ... }
we can say the following: the pointer passed to the
process () function must point to an object that implements the interface (inherit)
base, and the choice of implementations of the
prepare () and
work () functions will be made during the execution of the program depending on which object derived from
base type indicates
b .
If we consider the following code:
template<typename T> void process(T t) { t.prepare(); t.work(); }
then we can say that, firstly, an object of type
T must have the
prepare () and
work () functions, and secondly, the implementations of these functions will be selected at compile time based on the deduced real type of
T.As you can see, for all the different approaches, the main (from a practical point of view) common feature of both types of polymorphism is that the client code does not need to change anything when working with objects of different types, provided that they meet the requirements described above.
Since everything is so great, the code is not complicated in principle, the runtime overhead is leveled, why not completely replace dynamic polymorphism with a static one? Unfortunately, as is usually the case, things are not so simple. There are a number of both subjective and objective shortcomings of static polymorphism. The subjective ones include, for example, the fact that an explicit interface often simplifies the life of developers, especially in large projects. Having before your eyes a header file with a class - an interface that you need to implement is much more convenient than examining the code of template functions for what functions you need to implement and how to make this code work. Imagine, moreover, that this code was written a long time ago and now there is no one to ask what was meant in one or another piece.
Objective reasons can somehow be reduced to the fact that after instantiation, the template classes (functions) have different, often unconnected types.
Why is that bad? Objects of such types without additional tweaks (see
boost :: variant, boost :: tuple, boost :: any, boost :: fusion, etc.) cannot be put into one container and therefore batch processed. It is impossible, for example, to replace the object - a member of a class - with a different type of object, during the execution of the “Strategy” or “State”. And although these patterns can be implemented in other ways without class hierarchies, for example, using
std :: function or just
function pointers, the restriction is, nevertheless, on the face.
But no one forces us to strictly adhere to any one paradigm. The most powerful, flexible and interesting solutions arise at the junction of these two approaches, at the junction of the PLO paradigm and
generic paradigm. The
CRTP idiom is just one example of such a paradigm merger.
CRTP
CRTP (Curiously Recurring Template Pattern) is a design idiom, consisting in that the class inherits from the base template class with itself as a parameter of the template of the base class. It sounds confusing, but the code looks pretty simple.
template <class T> class base{}; class derived : public base<derived> {};
What can it give us? This design makes it possible to refer to the derived class from the base.
template<typename D> struct base { void foo() {static_cast<D*>(this)->bar();} }; struct derived : base<derived> { void bar(); };
And the possibility of such communication, in turn, opens up several interesting possibilities.
Explicit interface
In the chapter on static static polymorphism, I called the lack of explicit interfaces a subjective lack of static polymorphism. On this topic, you can argue, but somehow, an explicit interface is easy to determine using
CRTP . Indeed, we can determine the set of required interface functions through calls to these functions from the base class.
template<typename D> struct base_worker { void work() {static_cast<D*>(this)->work_impl();} void prepare() {static_cast<D*>(this)->prepare_impl();} }; struct some_concrete_worker : base_worker<some_concrete_worker> { void work_impl();
Using this design, one developer (architect) can define an interface of a certain set of classes, and the rest of the programmers will be guided by what to implement when implementing this interface. But
CRTP capabilities
are not limited to this.
Mixin
MixIn is a design technique in which a class (interface, module, etc.) implements some functionality that can be “mixed in”, introduced into another class.
By itself , the
MixIn class is usually not used. This technique is not C ++ specific, and in some other languages ​​it is supported at the level of language constructs.
In C ++ there is no native support for
MixIn 's, but nevertheless this idiom can be fully implemented using
CRTP .
For example, the
MixIn class can implement the functionality of a singleton or object reference counting. And in order to use such a class, it is enough to inherit from it with “me” as a template parameter.
template<typename D> struct singleton{...}; class my_class : public singleton<my_class>{...};
Why is CRTP here? Why not just inherit from a class that implements some of the functionality we need?
struct singleton{...}; class my_class : singleton{...};
The fact is that inside
MixIn 'and we need access to the functions of the inherited class (in the case of a singleton to the constructor) and
CRTP comes to the rescue
here . And if the example with the singleton seems far-fetched (really, who is using singleton today?), Then below you will find two examples closer to reality.
Enable_shared_from_this
MixIn structure
(boost) std :: enable_shared_from_this allows you to get a
shared_ptr to an object without creating a new group of ownership.
struct bad { std::shared_ptr<bad> get() {return std::shared_ptr<bad>(this);} };
In this case, each
shared_ptr obtained using the function
bad :: get () opens a new group of ownership of the object, and when it is time to destroy
shared_ptr 's, delete for our object will be called more than once.
Correctly do this:
struct good : std::enable_shared_from_this<good> { std::shared_ptr<good> get() { return shared_from_this();
This supporting structure is organized like this:
template<typename T> struct enable_shared { weak_ptr<T> t_; enable_shared() { t_ = weak_ptr<T>(static_cast<T*>(this)); } shared_ptr<T> shared_from_this() { return shared_ptr<T>(t_); } };
As you can see, here
CRTP allows the base class to “see” the type of the derived class and return the pointer to it.
Mixin function
MixIn functionality does not have to be included inside a class. Sometimes it is possible to implement it as a free function. As an example, we implement the “! =” Operator for all classes for which the == operator is defined.
template<typename D> struct non_equalable{}; template<typename D> bool operator != (const non_equalable<D>& lhs, const non_equalable<D>& rhs) { return !(static_cast<const D&>(lhs) == static_cast<const D&>(rhs)); }
As you can see, inside operator! = We use the fact that
non_equalable can use the “==” operator defined in the derived type.
You can use this
MixIn as follows:
struct some_struct : non_equalable<some_struct> { some_struct(int w) : i_(w){} int i_; }; bool operator == (const some_struct& lhs, const some_struct& rhs) { return lhs.i_ == rhs.i_; } int main() { some_struct s1(3); some_struct s2(4); std::cout << (s1 != s2) << std::endl; }
MixIn vice versa
Imagine that we are writing an implementation of the classes of gaming spacecraft. Our ships will move according to the same algorithm, except for some moments, for example, the mechanism for counting the remaining fuel and the current speed will differ from ship to ship. The classic implementation of the pattern template method (and this is it) will look like this:
class space_ship { public:
Now we will try to apply
CRTP .
template<typename D> class space_ship { public: void move() { if(!static_cast<D*>(this)->fuel()) return; int current_speed = static_cast<D*>(this)->speed();
In this implementation, we got rid of virtual functions and the code itself became shorter (no need to describe purely virtual functions in the base class).
The concept of
MixIn 'and with this approach is turned upside down. The main work is done in the base class, and we add the additional (different) functionality from the derived classes.
I want to focus your attention on this design technique and
Mixin 'ah in general. Do not be confused by the artificial example of spaceships or singleton. In real-world tasks, this approach allows building very flexible architectures, avoiding duplicate code, localizing the functionality in small classes and subsequently “mixing” them into the right mix at the moment. Especially, he begins to shine in cooperation with the means that allow a multitude of objects of different types to be batch-processed (see
boost :: fusion ).
MixIn Variations
The main software development theorem (
FTSE ) states: “Any problem can be solved by introducing additional levels of indirection”. Let's see how this can be applied to
CRTP MixIn 's.
You may have noticed in the previous chapters “Explicit Interface” and “On the contrary” MixIn I used public functions in a derived class. Generally speaking, this is not very good, since it breaks encapsulation. It turns out that the functions that are not intended for what the user called them directly “stick out” outwardly.
You can solve this problem by making the base classes friends of the derivative. After that, you can add these functions to the
private section, but imagine that you need to inherit from several basic
MixIn 's. Have to make friends all the base classes. For a comprehensive solution to this problem, as well as to ensure compilation on some old compilers, you can introduce a new level of indirection. It is a structure whose functions redirect calls from the base to the derived class.
struct access { template<typename Impl> static void on_handle_connect(Impl* impl) {impl->handle_connect();} template<typename Impl> static void on_handle_response(Impl* impl) {impl->handle_response();} };
Now from the base classes, we call not the functions of the derivative, but the functions of the intermediate structure.
template<typename D> struct connection_handler {
In the derived class, we only need to add as friends to the
access structure.
class combined_handler : public connection_handler<worker>, public response_handler<worker> { private: friend struct access; void handle_connect(){ std::cout << __PRETTY_FUNCTION__ << std::endl; } void handle_response(){ std::cout << __PRETTY_FUNCTION__ << std::endl; } };
Additional advantages of this approach include the fact that base classes no longer “know” about their derived classes, in particular, which particular functions need to be called, and a loosely coupled system, as a rule, is more flexible than the strongly connected one, and also all calls to the derived class are collected in one place (in the access structure), thus making it easier to visually separate them from the functions of the derived class that perform other work.
The downside, as is often the case, is the complexity of the design decision. Therefore, in no case do I call for the use of such a scheme both in the tail and in the mane, but it seems to me that it would not be superfluous to have an idea about it.