Upside Down Polymorphic Inheritance

Leveraging P2162 for Fun & Profit

The visitor pattern is an esoteric programming pattern that has never been widely popular. No one seems to be totally certain on what it’s good for, as demonstrated by the diversity of opinions found on StackOverflow. One notable quote from the software engineering StackExchange sums up the vibe:

Visitor pattern has the most narrow use and almost never will the added complexity be justified

The pattern was canonized in C++ with std::variant and std::visit in C++17. The reception was less than warm; in an influential post Mark Kline convincingly argued that, "std::visit is everything wrong with C++".

I’m partial to this argument. Plainly, we want pattern matching syntax, we want it now, and the longer it is delayed the more horrendous crust builds up in the gutters. Yet, pattern matching isn’t here and the crumbs down in the gutter look mighty tastey. Perhaps a little nibble wouldn’t be so bad…

In this post we’ll discuss how to use std::variant and std::visit as replacements for polymorphic inheritance, allowing the use of value semantics with a polymorphic type. No pointers, smart or otherwise.

If it Quacks Like a Duck

Kline argued that the usage of std::visit is overly verbose, requires expert-level C++ knowledge, or both. This remains indisputable, but the example used was crafted to show std::visit in the worst light.

Kline wanted to print a fundamental type’s name, thus every type needed to be matched exactly, but what if we weren’t dealing with fundamental types? Let’s consider Figure 1.

We don’t care if the type we’re dealing with is specifically Alice or Bob, we only care that the type is vaguely person-shaped, that it’s got a name().

The question becomes, what happens if we’re dealing with a type that doesn’t quack the way we need it to? Here we get in deep with “advanced” C++, though it looks a little different now than when Kline wrote his post.

Concepts, requires expressions, and constexpr-if are not beginner friendly, but Figure 2’s approach remains compact. We needn’t match every possible type, we need only identify if a given type conforms to our requirements via a concept.

Ok, we can std::visit with concise, if complex, code, that’s polymorphism covered. What’s this got to do with inheritance?

Tree of Types

The only type specified to work with std::visit is, naturally, std::variant. P2162 allows for one other kind:

[Types] that publicly and unambiguously inherit from a specialization of std::variant

The proposal discusses two advantages: recursive variant types, and the ability to “extend functionality” of std::variant. It’s this extension of functionality which provides a new angle on inheritance.

First an illustrative use case; consider a serialization protocol implemented with a virtual base class as in Figure 3. If you’re familiar with how this would be done using traditional polymorphic inheritance, skip down to Figure 5 for the good stuff.

Ignoring the problems that come from minimizing code length for example purposes, the fundamental struggle with virtual interfaces is the need to explicitly juggle heap allocations. The networking code interfacing with these classes would look something like Figure 4.

By modern C++ standards this is OK, but we’ve abandoned value semantics for the notational soup of smart pointers. P2162 provides an alternative approach.

Figure 5 lays out our packets as individual classes with no base class to derive from. The code is included here for completeness, but there’s nothing surprising in it.

Figure 6 brings the pieces together, we build a std::variant specialization of the “derived” classes from Figure 5 and inherit that specialization in our Packet “base class” (upside down!). Finally, std::visit implements the behavior of virtual functions.

Our networking code is now trivial (included as Figure 7 for completeness). We will probably ditch free-standing functions entirely and use the Packet interface directly. The major advantage is we can use value semantics while generically handling packets.

We’ve also ditched the id_val variable which served as our sum-type tag. This is now tracked implicitly by std::variant and won’t become a source of bugs.

Performance Gremlins

Before you go and turn your entire codebase upside down you should be aware that GCC has two infamous bugs which may impact the performance of std::variant and std::visit. As long as your total number of contained types is small, less than 11 elements, you’ll get a fast path thanks to a libstdc++ optimization. For larger type collections you’ll want to benchmark and see how much std::variant is going to cost you on GCC or switch to an alternative implementation such as mpark::variant

Final Thoughts

Ultimately I think this approach, while interesting for today’s software, is a hack. The true missing links here are Unified Call Syntax, with which we could extend std::variant types without the need to inherit from them, and Pattern Matching, which would replace calls to std::visit and their constexpr-if trees with a sensible syntax.

The idea that Haskell, a language where “whitespace doesn’t matter except for when it does” and strong claimant to the more-cryptic-than-template-metaprogramming throne, can express these concepts much more clearly than C++ is embarassing.