
Std::visit is everything wrong with modern C++ - foob
https://bitbashing.io/std-visit.html
======
maxlybbert
From the article:

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

You don't have to:
[http://en.cppreference.com/w/cpp/utility/variant/holds_alter...](http://en.cppreference.com/w/cpp/utility/variant/holds_alternative)
(and
[http://en.cppreference.com/w/cpp/utility/variant/get](http://en.cppreference.com/w/cpp/utility/variant/get)
to access the value).

~~~
masklinn
And now you get neither exhaustive checking nor type-safe unwrapping, at this
point is there really a point to variants? You may as well be using the old
enum+union.

(get_if at least nets you type-safe unwrapping similar to `if let` in Swift or
Rust, though it returns a pointer rather than a reference)

~~~
vinkelhake
You either want exhaustive checking or "just [want] to see if the thing
they’re looking at holds an int or a string". OPs comment was about the latter
and holds_alternative and get accomplishes that.

FWIW, std::get is type-safe in that you cannot specify a type outside of the
variant types. It's safe at runtime in that it will throw
std::bad_variant_access if the active object doesn't match the type.

And because of this, you think we may as well use enum+union? Even if you only
plan on manually type switching on a variant, std::variant saves you from a
lot of boilerplate.

------
trendia
> The fact that we still handle dependencies in 2017 by literally copy-pasting
> files into each other with #include macros is obscene.

Historically, C++ used includes so that it could be compatible with C. In the
future, modules can be used which avoid many of the problems with includes
[0].

[0] [http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2017/n468...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2017/n4681.pdf)

~~~
harpocrates
Is this implemented yet? The paper you linked is a draft. I'm glad C++ is
doing this.

> Historically, C++ used includes so that it could be compatible with C.

I think it is more the case that C++ slowly splintered off C and never broke
free completely of #include. Rust is also quite compatible with C without
supporting anything like #include. It even has a module system!

~~~
CyberDildonics
You are talking about limited binary compatibility instead of source
compatibility. These are two different things and can't be compared to one
another.

------
JoshTriplett
I agree completely with everything in this article. But in addition, I think
std::variant also misses the point of sum types in a big way.

Sum types don't just store values of different types. They store different
states, with associated data. So, for instance, consider the following
simplistic expression AST; how would you store it in a std::variant?

    
    
        enum Expr {
            Number(usize),
            Negate(Expr),
            Add(Expr, Expr),
            Sub(Expr, Expr),
            Let(String, Expr, Expr),
            Var(String),
        }

~~~
kraghen
The C++17 equivalent would be something like the following (not tested):

    
    
      using NumberExpr = int;
      using VarExpr = std::string;
      struct AddExpr;
    
      using Expr = std::variant<NumberExpr, AddExpr, VarExpr>;
    
      struct AddExpr {
        std::unique_ptr<Expr> a;
        std::unique_ptr<Expr> b;
      }
    

Of course, this being C++, you need forward declarations and a firm grasp of
the rules of incomplete types to be confident about declaring a simple AST
type.

~~~
tomjakubowski
To completely address JoshTriplett's point, yes, you can just define another
struct for the SubExpr variant to disambiguate it from the AddExpr case.

Requiring this kind of wrapping is awkward compared to e.g. Rust or Haskell's
treatment of sum types, which unlike C++17 and std::visit both have powerful
pattern matching features built into the language. Saying this as someone who
writes C++ all day: std::visit and std::variant are weaksauce.

~~~
twic
On the other hand, the C++ way gives you an actual type for each element of
the sum; you can write a function which only takes AddExpr. The Rust way
doesn't (yet).

~~~
saghm
Given that you have to define those types manually, I don't see why you
couldn't do the same in Rust; it just doesn't force you to if you don't need
it

------
any1
Sum types can actually be implemented quite effectively using X-macros in both
C and C++. In fact, I feel like they're simpler and more intuitive than this
variant stuff.

Edit: Let's use the same example.

    
    
      #define SETTINGS \
        X(string, str) \
        X(int, num) \
        X(bool, b)
    
      struct Setting {
        union {
          #define X(type, name) type name;
          SETTINGS
          #undef X
        };
        enum Type {
          #define X(type, ...) t_ ## type,
          SETTINGS
          #undef X
        };
        Type tag;
      };
      

Printing settings like in the example becomes this:

    
    
      void printSettings(const Setting& s) {
        switch(s.tag) {
        case t_string: printf("A string: %s\n", s.str.c_str()); break;
        case t_int: printf("An integer: %d\n", s.num); break;
        case t_bool: printf("A boolean: %d\n", s.b); break;
        }
      }
    

We can also load more things into the x-macro, so it's possible to define the
switch cases above just like in the structure definition. We could add a third
parameter called full_name:

    
    
      void printSettings(const Setting& s) {
        switch(s.tag) {
        #define X(type, name, full_name) \
          case t_ ## type: std::cout << "A " full_name ":" << s.name << "\n";
        #undef X
        }
      }
    

Disclaimer: I have not compiled or run any of this code.

~~~
jcelerier
there's absolutely zero compile time safety in your code. If someone copy-
pastes for instance

    
    
        case t_int: printf("An integer: %d\n", s.num); break;
        case t_bool: printf("An boolean: %d\n", s.num); break;
    

it will just break at runtime, unlike std::variant which prevents this at
compile time

~~~
any1
Yes, which is why I would always use the second example for the print
function.

Edit: In fact it is possible to handle the cases explicitly while enforcing
types by having the X-macros call functions in the switch/case and declaring
prototypes via the macros:

    
    
      #define X(type, ...) void printSettings_ ## name(type);
      SETTINGS
      #undef X
    
      void printSettings(const Setting& s) {
        switch(s.tag) {
        #define X(type, name) \
          case t_ ## type: printSettings_ ## name(s.name); break;
          SETTINGS
        #undef X
        }
      }
    

Then you define printSettings_... and have them do stuff with the data.

------
int_19h
The reason why we have std::visit the way it is, is because that's the way it
worked in Boost Variant, which is the basis for the proposal.

The reason why it's the basis, is because it's a time-tested, proven and
stable solution that has been around since 2002.

The reason why it's so ugly, is because that's the best you could do in C++
back in 2002.

So, there's a perfectly rational explanation for all this - it's not "insane".
It is unfortunate that they didn't come up with a better API that would make
use of new language features, but it's not like someone deliberately set down
to design the more convoluted older API just to confuse people.

------
option_greek
Half the way into the article, I felt like crying. Is it just me or is the
standards committee actively trying to reduce the number of existing C++
programmers. I think we are better off with boost than learning this new
stuff. I hope the people in standards committee will lose their
C#/Java/<insert cool language> envy and be more selective in what they want to
add to standards.

~~~
jff
> I think we are better off with boost

Whoa, let's not say things we can't take back ;)

~~~
copx
[https://twitter.com/id_aa_carmack/status/81104943490146304](https://twitter.com/id_aa_carmack/status/81104943490146304)

------
hoytech
I looked into this recently, and this is the implementation I ended up
choosing:

[https://github.com/mapbox/variant](https://github.com/mapbox/variant)

It has a very handy "match" method that addresses the issue raised in this
blog post.

They've also compiled many links to other variant implementations, as well as
a bibliography on the standardisation efforts:

[https://github.com/mapbox/variant/blob/master/doc/other_impl...](https://github.com/mapbox/variant/blob/master/doc/other_implementations.md)

[https://github.com/mapbox/variant/blob/master/doc/standards_...](https://github.com/mapbox/variant/blob/master/doc/standards_effort.md)

------
bluejekyll
> 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?)

These are great questions for any piece of software. Especially features being
introduced in a language.

~~~
pas
Why would you teach someone C++, a language that has a lot of problems because
it started as C with some sugar on top and then against all odds went ahead
and added a ton of things that are very un-C-ish while still supporting all
the C-ishness it had (has).

Teach new languages to new programmers. Then when they have firm grasp on
these concepts, and for some cruel trick of fate they have to do C++
development, then they can look this up.

------
tlb
You can use variants just fine without a visitor. Like,

    
    
      std::variant<string, int, bool> v;
      ...
      if (auto pstr = std::get_if<string>(&v)) {
        cout << *pstr << endl;
      }
      else if (auto pint = std::get_if<int>(&v)) {
        cout << *pint << endl;
      }
      else if (auto pbool = std::get_if<bool>(&v)) {
        cout << *pbool << endl;
      }
      else {
        count << "null" << endl;
      }
     

which is not much worse than the pattern matching syntax.

~~~
ezyang
You don't get exhaustiveness checking, which is the killer feature of
variants.

~~~
joe_loser
Yeah, plus the if branches are runtime-checks instead of the if-constexpr
approach

~~~
gpderetta
There is about the same amount of runtime checks. In the auto-lambda+constexpr
case the switch/if-cascade is inside visit and it is required to dispatch the
correct type to the visitor.

------
krona
I don't know why the author is so disparaging of _if constexpr_ ; I think it's
going to push c++ metaprogramming in a direction that makes the more powerful
aspects of the language easier to understand for beginners.

Surely a good thing.

------
AlexCoventry
The struct solution seems OK to me, and the alternatives don't seem to
accomplish much, brevity-wise. What's the problem with it?

~~~
xwvvvvwx
You would need the lambdas if you wanted to capture state from outside the
visitor.

But I agree, the struct seemed fine (not amazing, but good enough) to me.

------
ChrisSD
So the short version is that the C++ committee accepts a partial feature set
despite key features not being ready in time?

Perhaps they expect Boost to work round it until the next standard is
released...

~~~
blub
std::variant is fully featured, it's just that the convenient lambda-based
make_visitor is missing.

------
slavik81
So, if you really hate making a struct each time, you drop make_visitor into
the project's util.h and never worry about it again. I don't see the big deal.

~~~
chombier
Besides, actually making a struct everytime is not that bad in practice.

I also understand (and strongly support) the committee's desire not to
introduce yet another language construct when a library solution can be worked
out, given the horrible beast c++[11,14,17,20] has already become.

------
kxyvr
Funny enough, std::visit was one of the features of c++17 that I was looking
forward most to. Something that I don't entirely understand, though, and the
author brings up is the lack of a function like make_visitor. Does anyone
understand why or was there a late addition of a function that I'm not aware
of? And, yes, std::visit is nonideal, but I do think it to be a much better
option to using double dispatch and the visitor pattern, which is what we had
to do before.

More generally, I write numerical software and I do with there was a better
option for writing this software outside of C++, but I don't see one right
now. Specifically, C++ gives us direct access to the c-api of other languages
and some pretty powerful tools to handle that. As such, if we want our
software to work across multiple languages like Python, MATLAB/Octave, or a
variety of other languages, C++ appears to be the best fit. Yes, it's possible
to hook something like a Python code to MATLAB/Octave, but it's hard because
Python and MATLAB/Octave handle memory in different ways. For example, they
differ on how and when objects are collected by the garbage collector, so it
makes it hard to use a Python object directly in MATLAB/Octave. In C++, we
have enough tools to handle hooking C++ objects and items to other languages.
Certainly, it's a pain, but I contend it's easier than to hook two other
languages together through the c-api, but I find this more difficult to manage
and we now have a bunch of additional code to maintain as well. As such, as
many disadvantages as the language has, I appreciate new features like
std::visit because it means that I can write easier code for my algorithms and
still be able to hook to others.

~~~
shpongled
I don't want to sound like a Rust evangelist... but it might be worth looking
into. You get nice C FFI + some really nice features.

~~~
kxyvr
Actually, that's an interesting thought. I just did a quick check of the FFI
and it looks promising. Thanks for the the idea!

------
bitL
Seems like a design pattern from Java which often are badly converted
functional programming snippets into OOP. I once loved C++, now I can't force
myself to read the code anymore... Please add some monads there to destroy my
hope for humanity once for all!

I think maintainers of C++ have a case of "functional programming" envy.

~~~
dfox
Discriminated union is low-level pattern from non-OO languages (it is common
pattern in C, Pascal has syntax for this and IIRC unions are always
discriminated in "standard" Pascal)

In object oriented languages this pattern is mostly unnecessary, because
inheritance is usually better solution for the same problem. Only reason to
use something like this in C++ is (maybe even only perceived) efficiency
gained by removing level of pointer indirection.

~~~
xyzzyz
The sum type, which the author is talking about, is not the same as
"discriminated union" pattern, the latter being the crude implementation of
the former.

It really comes from functional languages like ML, and is extremely useful and
convenient when combined with pattern matching. In fact, attempts to emulate
it in OOP using interfaces and visitor pattern tend to bring a lot of
boilerplate, and obscure the actual logic, which is exactly what the author is
complaining about.

There's definitely more reason to use it than just "efficiency gain", which is
why many languages introduced in the last decade have it built in (e.g. Rust,
Scala or Swift).

~~~
mcphage
> The sum type, which the author is talking about, is not the same as
> "discriminated union" pattern, the latter being the crude implementation of
> the former.

Could you say something about the difference between the two? I'm not aware
what it is.

Edit: wow I accidentally a word.

~~~
mrkgnao
IIRC discriminated unions are plain C(++) unions with an added type tag, like
at the beginning of the article. Brittle, prone to breakage when refactoring,
etc. when compared to real sum types.

------
Davidbrcz
Quick survey

    
    
        std::variant<std::string,bool> v {"abc"};
    

Q: Which type is used ?

(Since I'm asking a question, you know there is a gotcha). Answer: "abc" is
const char*, which can be converted to bool and is picked because the way
variant is designed.

------
mark-r
I've been programming C++ for 20 years, and since C++11 the only feature I've
seen that's worth the cognitive load is _auto_. Everything else just seems
overly complicated. When I compare it to how easy things are in Python, I cry.

~~~
humanrebar
The syntax might seem strange to you, but this is pretty slick:

    
    
        bool batch_failed = std::any_of(
            jobs.begin(), jobs.end(),
            [](const auto & job) { return job.failed(); }
        );
    

In C++03-plus-auto, you'd have to define an helper function or class:

    
    
        bool isJobFailed(const Job& job) {
            return job.failed();
        }
    

And then use find_if, I guess:

    
    
        auto last = jobs.end();
        auto iter = std::find_if(
            jobs.begin(), jobs.end(),
            isJobFailed
        );
        bool batch_failed = (iter == last);

~~~
Koshkin
Doesn't look pretty, does it? Now compare this with what it could have been:

    
    
      jobs.Any(job => job.failed())
    

(Lambdas in C++ is one ridiculous example where I must use all the existing
types of brackets in one expression:

    
    
      [](){}
    

is a lambda.)

~~~
nly
What is 'jobs' in your pseudo-code? Can it be a user-defined type? Does the
author of that class need to explicitly state that their type meets some
trait? Is 'Any' part of the type, a trait, or the language?

What _humanrebar_ wrote is an algorithm that will work with any range of any
type that has a 'failed()' member function.

Here's another formalisation:

    
    
        auto batch_failed = [](auto const& batch) {
            return std::any_of (begin (batch), end (batch), 
                               [](auto& testcase) { return testcase.failed (); });
        }
    

This function will basically work with anything.

~~~
Davidbrcz
You can't argue that the current C++ syntax is verbose and cumbersome.

99% of the time, I work on the full container, why not providing an overload
which let me write at least

    
    
        bool batch_failed = std::any_of(jobs, 
            [](const auto & job) { return job.failed();}
        );
    

Then, lambas are verbose. I would like to have a simpler syntax like for
simple lambas (no capture, single expression in the lamba body). job could be
automatically typed with const auto&, or you could write it yourself if you
wish.

    
    
         job => return job.failed();
    

And a the current one, which is more verbose, for more complex lambas
(capture, several expressions in the body)

------
ausjke
the new c++ is great in that it is evolving fast, but I feel it is extremely
hard to read, the readability of the language is diminishing by each newer
version.

~~~
PopsiclePete
std::<I<std::?<dont[know=what]>>(you) << mean >>
std::it<looks,ok,to>(?auto=me)

------
lomnakkus
I think all the answers will probably come when C++17 support is good enough.
See the example at [1], in particular the "overloaded" bit.

I _think_ my current GCC 7.2 should support it, but I don't think my current
Clang 4.0.1 does. EDIT: Just tested and I can confirm that.

[1]
[http://en.cppreference.com/w/cpp/utility/variant/visit#Examp...](http://en.cppreference.com/w/cpp/utility/variant/visit#Example)

------
kelvin0
Javascript and other dynamic languages are leaning towards type safety and
other static checking tools. C++ and other compiled languages have strong
types , but need and std::variant (and similar constructs) to add flexibility
and boost productivity. The Yin and the Yang, looking to balance themselves
into perfect programmer bliss.

------
amluto
I had a perfectly functional lambda-based visitor working in C++11. I don't
see the problem. I can try to dig it up.

------
lsh123
I might be missing something big but the "match (theSetting) {...}" example
can be mapped to C++ as:

switch(setting->tag) {

case Str:

    
    
      // do something with setting->str
    
      break;
    

case Int:

    
    
      // do something with setting->n
    
      break;
    

case Bool:

    
    
      // do something with setting->b
    
      break;
    
    }

~~~
hoytech
One thing that a match method can provide is a compile-time check that you
haven't forgotten to handle any cases of your variant.

~~~
snowAbstraction
The Clang C++ compiler will warn if the switch is not exhaustive.

~~~
lsh123
gcc will warn too

------
makecheck
No doubt that std::visit appears to be a mess but why not call
std::get<a_type>(a_variant) and trap bad_variant_access exceptions? It would
still be a bit annoying to exception-wrap each attempt but it seems cleaner
than any of the std::visit alternatives.

~~~
MontagFTB
Using exceptions as flow control is an anti-pattern in many modern C++
developer circles. It would also be excessively expensive.

------
jasonmaydie
Is the author complaining that the process is too complicated? This is typical
of the c++ world, other languages make it easy but c++ isn't other languages.

------
iammyIP
C++ is a meta language based on C. You can pick the features you want to use.

------
Animats
"Sum types". Hey, let's give an old concept a new name. Those are called
discriminated variant types, and they first appeared in Pascal. They're a
straightforward concept, and can be implemented easily at the language level.

The big problem with C++ is that the template fanatics took over. Templates
are a crappy programming language - bad syntax, confusing semantics, and tough
debugging. But there's no way to stop people from extending the language via
templates. Hence Boost, and "you are not supposed to understand this"
templates.

(I fear that Rust is going down the same rathole.)

~~~
jnbiche
> "Sum types". Hey, let's give an old concept a new name.

Sum type has been the standard functional programming language term for this
concept since at least the 1970s, perhaps earlier.

------
clishem
This blog post is not to be taken seriously in my opinion. For one thing,
`printf` is obsolete in C++. This is how you should output values in a tagged
union:

    
    
        std::variant<size_t, std::string> var = "test";
        std::visit([](auto &&val) { std::cout << val; }, var);
    

If you also want to print the name of the type of the variable, along with the
value itself, well, C++ doesn't have reflection (yet). So you're going to have
to write a function that takes a value and returns its type as a string, which
you can already do using the typeid operator. This is basically implementing
reflection yourself, it is cumberstone but doesn't require advanced template
programming.

There no need for any of the madness with explicit types inside the visitor
lambda, which is deemed as neccesary by the author.

Edit: here's another way to map types to strings. So no templates necessary at
all for this entire problem.

    
    
        std::map<std::type_index, std::string> typeIdxToString;
        typeIdxToString[typeid(std::string)] = "string";
        typeIdxToString[typeid(int)] = "int";
        // ...
    

Edit2: Turns out C++ has some form of reflection after all. You can just use:

    
    
        typeid(val).name()
    

If your compiler supports pretty names.

~~~
coldtea
> _This blog post is not to be taken seriously in my opinion. For one thing,
> `printf` is obsolete in C++. This is how you should output values in a
> tagged union:_

This is so irrelevant as to the point of the article that it is funny.

~~~
clishem
Glad you enjoyed my comment then.

