std::visit is everything wrong with modern C++

Sum Types and You

A sum type, also called a discriminated union,
can hold one (and only one) of several types of things.
For example, consider some settings in an
INI-like configuration file.
Let’s say that each setting must be a string, an integer, or a Boolean value.
If we wanted to roll our own solution in C++, we might write something resembling:

Call constructors and destructors at appropriate times for all non-trivial types.
(string is the only one here, but you could imagine similar
scenarios with others.)

If a step is ever forgotten, the object falls into an
inconsistent state and there shall be wailing and gnashing of teeth.
You could encapsulate all this trickery and interact with the type
through a series of methods—e.g., getType(), asBool(),
asString(), and so on—but this is quite verbose.
It also just shifts the problem onto whoever implements these methods; they
still need to carefully maintain the invariants with no help from the language.

It would be much nicer if a general-purpose sum type was provided by the standard
library.
In C++17, we finally get one!
It’s called std::variant.
Let’s take a look.

Using std::variant

variant is a class template that takes, as template parameters, the types
it could hold.
For the example above,
we could define a setting as a variant<string,int,bool>.
Assigning a value to a variant works just like you might expect:

Once we put a value into a variant, we’ll eventually want to look at what that
value is, and just as importantly, what the type of the value is.
This is where the fun begins.
Some languages offer dedicated pattern matching syntax for the task,
such as:

but this didn’t make the cut for C++17.2
Instead we’re given a companion function called std::visit.
It takes the variant you want to examine, along with
some visitor that is callable for each type in the variant.

How do we define such a visitor?
One way is to create an object that overloads the call operator
for relevant types:

This seems terribly verbose, and it gets even worse
if we want our visitor to capture or modify some other state.
Hmm—lambdas are perfect
for capturing state.
What if we could build a visitor from those?

Here we use C++11’s variadic templates.
They must be defined recursively, so we create some base case F0,
then use that to define a cascading set of constructors for overload,
each of which peels off a lambda argument and adds it to the type
as a call operator.

If this seems troublesome, fear not! C++17 will offer a new syntax
that reduces all of the above to:

No.

The rigmarole needed for std::visit is entirely insane.
We started with a simple goal: look at the contents of a sum type.
To accomplish this meager mission, we had to:

Define a function object, which requires a lot of
boilerplate, or

Define our behavior with lambdas, which required:

An understanding of variadic templates, in all their recursively-defined fun, or

A familiarity with variadic using declarations, fresh on the scene from C++17.

or

Use compile-time conditionals, which require you to know
about—and grok—the new constexprif syntax, along with
type_traits fun like
std::decay.

None of these concepts are too enigmatic if you’re an experienced C++ developer,
but several are certainly “advanced” features of the language.
Things have really gone sideways if we need to know so much
to do something so simple.

How did we get here?

My goal isn’t to disparage the folks on the ISO C++ committee
who picked this approach.
I’ve had beers with some of them,
and they’re smart, kind, hardworking people.
I’m sure that I’m missing important context since I’ve never sat in on a
standards meeting or read all of the relevant
committee papers.
But from an outsider’s perspective, the disparity in complexity between the
problem being solved (“What’s in here?”)
and the solutions is just nuts.
How do you teach this without overwhelming a beginner with all this other…
stuff?
Is it expected to be common knowledge for your everyday programmer?
(And if the goal of adding variant to the standard library isn’t to
make it a tool for the masses, shouldn’t it be?)
The very least C++17 could do—if the committee didn’t have the time or resources
to get pattern matching into the language—is provide something akin to make_visitor.
But that too is left as an exercise for the user.

If I had to guess how we ended up this way,
I’d assume it comes down to confirmation bias.
Maybe when a bunch of really smart people who know how
SFINAE works offhand
and don’t flinch when they see the likes of

get together, the result is something like std::visit.
Nobody proclaims that the emperor has no clothes, or that it’s completely
bonkers to expect the average user to build an overloaded callable
object with recursive templates just to see if the thing they’re looking at
holds an int or a string.

I’m also not here to claim that C++ is too complicated for its own good,
but it’s certainly more complicated than it has to be.
Scott Meyers, the guy who wrote Effective C++ and Effective Modern C++,
has made similar noises in recenttalks.
To paraphrase Meyers, I’m sure each member of the committee cares very much
about avoiding needless complexity and making the language easier to use.
But if you look at the results of their work, it’s hard to tell.
The accidental complexity just keeps stacking up.

Where are we headed?

There’s a reason C++ is so widely used, especially in systems programming.3
It can be incredibly expressive, yet gives you nearly full control of your hardware.
The tooling around it is some of the most mature of any programming language
out there, bar C.
It supports a ridiculous number of platforms.

But even if you set aside all the historical baggage, it has some serious shortcomings.
Spend any amount of time messing with D and you’ll quickly realize that
metaprogramming needn’t require self-flagellation and insane syntax.
Play with Rust and you’ll feel like unique_ptr
and shared_ptr—which themselves have been a breath of fresh air—are
a bad joke.
The fact that we still handle dependencies in 2017
by literally copy-pasting files into each other with #include
macros is obscene.

You get the impression, based on what ends up in the ISO standards and what
you hear in conference talks,
that those driving C++ are trying to eliminate some of these shortcomings by
glomming nice bits from other languages onto it.
That’s a great idea on its face,
but these features often seem to arrive half-baked.
While C++ isn’t going away any time soon,
it feels like the language is constantly playing a clumsy game of catchup.

In spite of all of this,
I’ll be busy encouraging my coworkers to use variant if anybody needs me.
Sum types are such a useful concept that they’re worth the pain,
and to quote Jon Kalb,
“If you can’t program in a language with ugly warts, maybe C++ isn’t the language
you should be programming in.”

The term “sum type” comes from the underlying type theory—if a type
can hold either type A or type B, its set of possible states is the
sum of all possible states of A and all possible states of B,
A + B.
You are already familiar with the dual of sum types: product types,
AKA structs, tuples, etc.
(Consider that the set of possible states of a struct containing types A
and B is the product of their possible states, A × B.) ↩

See language proposal
P0095R1
for the current work on pattern matching in C++. ↩