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 tasty. 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.