Replacing virtual with concepts in C++20
While designing and writing C++ libraries/applications, dynamic polymorphism is a very useful tool.
We have all used Pure Virtual classes to define specific requirements for an Interface, or if we want to store multiple derived types of a common base class within a container. These are some of the common examples of a myriad of uses one employs dynamic polymorphism in. As C++ developers, we have grown quite accustomed to it and consequently, it is fairly intuitive to think of design or code in dynamic-polymorphism terms. Also, for most cases it works well and as expected.
However, there may be cases where one may desire to avoid using virtual functions. For e.g., if there are very stringent performance requirements, one may want to avoid pointer indirections that comes with using virtual.
With the introduction of concepts in C++ 20, now it is possible to mimic the behaviors one achieves with dynamic polymorphism while also moving it to compile time.
To demonstrate this, I tried to implement the Visitor design pattern using C++20 concepts and compare it with the traditional implementation.
The use case is, I have a collection of different financial instruments (for e.g., Equities and Bonds) and based on some logic we want to either place a Buy-trade or a Sell-trade for these instruments.
The Visitors are “BuyInstrumentVisitor” and a “SellInstrumentVisitor”. When the Buy visitor visits any of the contained instruments, it places a buy order for it and correspondingly the Sell visitor places a sell order.
Let’s first look at the traditional implementation done using Virtuals.
First, we have the Instrument Interface which has an accept method for the Visitors (InstrumentInterface.h).
Then, we have the implementation classes for different instruments.
Bond.h
Bond.cpp
And Equity.h
Equity.cpp
These concrete implementations, override the accept() method and call the visit on the Visitor passed as argument.
Next, we have the Visitor Interface (VisitorInterface.h) which must have a visit() method for all the different instruments.
Recommended by LinkedIn
And then the Visitor Implementations.
BuyInstrumentVisitor.h
BuyInstrumentVisitor.cpp
And SellInstrumentVisitor.h
SellInstrumentVisitor.cpp
Now, suppose there is an Instrument Manager, which holds a collection of these instruments in a vector and we populate this vector with the instrument instances.
And then call different Visitors on them.
Output
This was the traditional Visitor pattern Implementation using Dynamic Polymorphism. Let us now see an implementation using C++ Concepts.
Here, instead of defining an Instrument interface, we will define an IInstrument concept. This concept defines the various interface specifications that any Instrument type must adhere to.
InstrumentInterface.h
Next, we define an Instrument Manager class, which is a class template which takes in Variadic number of template arguments. This means that any number of desired Instrument types can be passed at compile time during class instantiation. However, we have added a constraint that it only takes in Types that satisfy the IInstrument Concept, otherwise a compilation error is thrown.
To store the instances of the different instrument types, it uses a tuple of vectors.
We then add a method to the InstrumentManager class to add instrument instances to our collection defined above. This method also has an additional constraint that the passed instrument type must be one among the types that was specified initially during InstrumentManager instantiation. This is taken care by the is_one_of_ins concept defined above in InstrumentInterface.h. If the passed instrument type does not satisfy this constraint, a compilation error is thrown.
We also add a method to instantiate the VisitorManager with all the desired Visitor Types, which in our case is the BuyVisitor and the SellVisitor. We will see later the definition of the VisitorManager class. Notice that this is a function template which has the added constraint that the Visitor Types specified must satisfy the IVisitor concept which we will see shortly.
Next, we define the accept() method, which is again a function template and it takes as template argument, the type of Visitor to apply.
In our case, this Visitor will be applied to all the instances of the instruments that the InstrumentManager contains. For this we will also need some helper code.
The complete InstrumentManager.h class is as shown below
Similarly, for the Visitor Interface we define an IVisitor concept which specifies the interface requirements for defining a visitor type.
Just like the InstrumentManager, we have the VisitorManager class, which is again a class template taking a variadic number of visitor types. These types are constrained by the IVisitor concept and thus we can only pass types which satisfy the interface requirements set by the concept.
For storing the instances of the specified visitor types we again use std::tuple. it also has a visit() method, which takes as template argument the type of the visitor to apply, and the instrument on which to apply the visitor, as the function argument. The complete code for VisitorManager.h is as below
The concrete instrument classes i.e. Bond.h/Bond.cpp and Equity.h/Equity.cpp remain same as shown above (in previous example), just that they don't require the accept() method any more.
The concrete visitor classes i.e. BuyVisitor.h/BuyVisitor.cpp and SellVisitor.h/SellVisitor.cpp remain exactly the same.
The main function can be as shown below.
Output: