Using C++17: std::variant for static polymorphism
published at 11.10.2020 16:41 by Jens Weller
Save to Instapaper Pocket
A few weeks ago I wrote about some code I was refactoring from single to multiple purpose. This is the second part looking at how to utilize C++17 for this.
In the previous post I wrote about how I'm refactoring a program to be used for more then one thing. Though I'd like to focus on the new things to write, instead of refactoring everything into a big class hierarchy. The old code gives me many of the interfaces to use and places I need to change in order to achieve my goal. Part of my goal is also not to touch the old code too much. Its very specialized, so that I can't reuse most of it.
std::variant and static polymorphism
Static polymorphism allows you to use generic code to share the same interfaces, but run on different and unrelated types. The classes A and B are different, don't have a common base class. Yet they both can run on generic code the same way, as long as they share the interface. With concepts this can be ensured in a very user friendly way, C++17 and 14 also have their means to do this. But as I'm not writing a library taking various types, I'm not going to go into this topic. Once C++20 and concepts are widely available, I might revisit to use a concept though.
But, lets say you have a variable, that needs to store the different types, and you are not in generic code. Like when you refactor a program, and now would like to store either A or B in the same type in a non templated context. This is where std::variant comes in. std::variant lets you define a list of types that can be stored in the same variant. std::variant<int,float,double> stores either an int, float or a double. So you can see a variant as the equivalent to a base class pointer. Instead of having a vector of base class pointers (e.g. vector<BaseShape*>), you'd have a variant with the types using the interface (e.g. vector<variant<Rectangle,Circle,Triangle> >). Though in the current case, no container of different types at runtime exists (yet).
In my case thats currently
using t_shapegroup = std::variant<penrose::PenroseShapeGroup,ShapeGroup<cpp20tshirt::RasterizedShape>>;
Everything in the namespce penrose is the old code, the 2nd type the new base type for creating a "rasterized" shape. Right now its only able to do this, I'd like to add additonal options to have color patterns with in the created rasterrized shapes.
Intialization and first usage of the variant
At runtime, the program needs to know which mode it is in, and create the right type. A factory type could make this in a fancy version easier, for the moment I went with an enum and a simple switch to instantiate the right type and assign it to the variant holding the processing type.
filter = QImage(file); auto pgr = new PixelGroupRunner(filter,this); QThreadPool::globalInstance()->start(pgr ); connect(pgr,&PixelGroupRunner::finished,this,[this](const PixelModel& m){ t_shapegroup shapes; switch (mode) { case ShapeType::PENROSE: shapes = penrose::PenroseShapeGroup{}; break; case ShapeType::RASTER: shapes = ShapeGroup{}; break; } //penrose::PenroseShapeGroup psg; m.visit([&shapes](auto& pg){ std::visit([&pg](auto& v){ using T = std::decay_t<decltype(v)>; if constexpr (std::is_same_v<T, penrose::PenroseShapeGroup>) v.addShape(penrose::PenroseShape(std::move(pg))); else if constexpr (std::is_same_v<T, ShapeGroup>) v.addShape(cpp20tshirt::RasterizedShape(std::move(pg))); },shapes); }); ui->tabWidget->addTab(new StepWidget(std::move(shapes),ui->tabWidget),QString("Mask %1").arg(ui->tabWidget->count())); });
Though this code is a bit more complex. It groups all pixels of the mask color into one or more group, a pixel group (pg) is a set of pixels that is connected to each other. The letter ö is three groups, one for each dot and one for the o. Each of these pixel groups is then moved into what is then creating shapes within the boundary of the pixels contained in the pixel group.
When PixelGroupRunner finishes, all pixel groups are held by pixel model. Which offers a visit method that allows to visit them all. And as this code is moving on to the second step, its moving each group into a specialized class for holding these pixel groups.
And this specialized class is in the variant. Hence now the argument of the lambda given to PixelModel::visit will visit the variant with std::visit. In this case I decided to go for a simple generic visitor, which simply then determines its currently active type by comparing the type with if constexpr to the given argument v. Its not the most elegant solution, cppreference has some examples for the options you have when using std::visit. I think in the future I will give the overload template a try.
Further examples
The code then goes on to construct the right wrapping type and moves the pixel group into it. And at the end the variant it self gets moved into a widget, which will draw the shapes created.
And in this widget class there is again 2 occurences of std::visit, once it sets a lambda as a call back to do the drawing, which is different for each of the types in the variant. Also it needs to be able to handle the drawing with two different painters: a normal Painter and a SVGPainter.
The program can control the progress manually, as each time the algorithm is run only once. Hence each type has a step method to generate the next generation of the algorithm creating a shape:
std::visit([](auto& v){v.step();},sg);
In this case only a single line is needed to satisfy the current and all future types that have the needed interface. Currently thats not very interesting for the rasterrized version, but I plan to introduce a 3rd option based on the rasterized code, which then is able to execute a new generation which each step, and with that change the color pattern in some of rasterized squares.
You can find the current version of the program on github.
Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!