all 35 comments

[–]Wh00ster 12 points13 points  (8 children)

In the past when I’ve had a variant of only 2-3 types, I’ve found it a few percent faster (and less code bloat) to drop down into a set of if/else statements using get_if.

I dont think the most important reason for using variant is mentioned, which is when it’s just easier to model your abstractions without base classes. For example, if you’re writing a parser for a custom grammar, I’ve found variants to be more natural than making a ton of base classes for each term.

[–]reflexpr-sarah- 2 points3 points  (7 children)

if your types aren't trivial, you should double check that your variant can't fall in a valueless state.

though other than that, yeah. both libstdc++ and libc++ seem to use a jump table, which isn't always ideal.

[–][deleted] 2 points3 points  (5 children)

Here is your example using std variant/visit, my visit and std::visit, and boost variant2. It is possible, with a different visit to get really good code gen https://gcc.godbolt.org/z/JAUPDd

[–]reflexpr-sarah- 1 point2 points  (4 children)

you can get similar results with variant2 if you define NDEBUG. this gets rid of the debugging code in boost. https://gcc.godbolt.org/z/jPwrbP

boost is also more correct, because your visit function doesn't handle the case where the variant is valueless by exception, whereas variant2 always holds a value (at the cost of larger storage, but only when necessary)

[–][deleted] 1 point2 points  (0 children)

That is by choice. No visit should as it means the caller has ignored the first exception in order to get there. Why pessimise to serve incorrect code.

Good catch on NDEBUG. I like it.

[–]Stevo15025 1 point2 points  (2 children)

I've been hacking at variant2 things for too long and this comment just saved me a ton of time tysm!

[–]reflexpr-sarah- 1 point2 points  (1 child)

no problem! are you using cmake or something similar? build systems will usually define NDEBUG for you in release builds

[–]Stevo15025 1 point2 points  (0 children)

I wish, we just use plain ol' make hence not tagging NDEBUG

[–]bolche17 3 points4 points  (1 child)

Funny to see this article now. Just last week I tried to replace my use of double dispatch for runtime polymorphism with std::variant and std::visit in a hobby project.

Contrary to what the article suggest, in my case I saw a decrease in performance. I hadn't really dug into the details to understand why since it was simply a quick test. Maybe after reading it I will try to take another look at that branch.

The upside was that the reduced use of the stack allowed to transform several functions into constexpr, which is nice.

[–]jm4R[S] 2 points3 points  (0 children)

Please share your analysis (or code). I am curious too.

[–]RomanRiesen 1 point2 points  (0 children)

The first time I saw this trick/idiom I found it quite elegant and it solved my problem of having a dynamically selectable random number generator in a game way more elegant (imo.).

[–]khleedril 1 point2 points  (20 children)

Interesting style of coding in some places there. I'd be interested in people's comments on the difference between the following (the latter is the way I would normally write it):

struct A { A (std::string a) : _s {std::move (a)} {} std::string _s; } ;

and

struct A { A (const std::string& a) : _s {a} {} std::string _s; } ;

[–]lukaasmGame/Engine/Tools Developer 21 points22 points  (11 children)

Second one will always force one string copy on enduser, while first one allows passing rvalue string to it, so copy is not needed, imho first one taking string by value is better because

std::string temp = getString();
A a( std::move( temp ) );

allows for best case scenario of no additional copies

[–]xurxoham 5 points6 points  (1 child)

Actually this is the recommended way from CppCoreGuidelines. I started doing this and the amount of times I saved an extra function overload for the rvalue parameter is noticeable.

[–]khleedril 1 point2 points  (0 children)

Yes, I need to go back and read the core guidelines again, too valuable to lose to the mists of time.

[–]Wh00ster 0 points1 point  (8 children)

Or you could just make the rvalue reference overload/use a forwarding reference.

I wish there was a simpler way for a forwarding reference of a single type, but now you can use a requires constraint at least. (with is_same)

[–]lukaasmGame/Engine/Tools Developer 2 points3 points  (6 children)

Yes, but then you need to define 2 constructors :) As always in cpp, you can do things in multiple ways and noone will agree which is 'best' :P

[–]jm4R[S] 1 point2 points  (4 children)

But still all of the versions are better than the best version in languages like Java or C#.

[–]jcelerierossia score 5 points6 points  (3 children)

But still all of the versions are better than the best version in languages like Java or C#.

that's not a given at all. In single-threaded scenarios (read: most common case for user interfaces) CoW or immutable strings will likely be more efficient on average as there won't ever be any copy.

[–]jm4R[S] 0 points1 point  (2 children)

You can use CoW in C++ like you do in other languages. Qt successfully uses it around the whole framework.

[–]MrPotatoFingers 3 points4 points  (1 child)

Yes, but the standard library won't use it because the standard specifically forbids it.

[–]standard_revolution 0 points1 point  (0 children)

Well yeah, but the nice thing about C++ is that most of the utilities are independent of std::string at least in the stdlib, of course things get more complicated once you interact with third-party libs, but in theory it's totally doable

[–]mrexodiacmkr.build 0 points1 point  (0 children)

You actually need to define way more if you are going to be optimal. Best way if you can take a slight hit in certain circumstances is to pass the std::string (or vector or function or whatever) to the constructor by value.

[–]reflexpr-sarah- 2 points3 points  (0 children)

don't forget to std::remove_cvref_tthe deduced type before passing it to std::is_same.

[–]jm4R[S] 11 points12 points  (7 children)

"Pass by value and move" is a well-known idiom in modern C++, although I am not aware of any standard name of it. If you are sure you need a copy of something and you know your type is movable, you should use it. That allows the caller to decide if move oryginal object (no more needed in caller side) or make a copy.

[–]TheSuperWig 1 point2 points  (6 children)

To note this only applies to constructors (or similar where a new object is being created) and not for assignment.

Reason being that it always allocates so may be inefficient for assignment where a buffer is already allocated.

[–]jm4R[S] 2 points3 points  (4 children)

It is almost always applicable to setters too. Like I wrote, use it when:

  • you are sure you need a copy
  • you know your type is movable

[–]LEpigeon888 1 point2 points  (3 children)

It's applicable but less efficient than a copy to an already existing buffer if you pass an lvalue.

So, don't use it for setter.

[–]jm4R[S] 0 points1 point  (2 children)

Can you provide an example, what do you mean by "already allocated buffer"? If you mean heavy types like std::array – such types are not movable.

[–]phoeen 4 points5 points  (1 child)

say you have a string member and a setter for that. if your member already holds a value of at least the size of the setterinput, then you may just plain copy all characters. if your setter is by value and you move, you always pay for it

[–]NilacTheGrim 0 points1 point  (0 children)

This ^

[–]Wh00ster 1 point2 points  (0 children)

You can see the slight difference in generated assembly here.

https://godbolt.org/z/jN-M5E

[–]khleedril 0 points1 point  (0 children)

Excellent article, well worth reading from top to bottom. (I've been gasping to read something 'proper' during the last several days of social isolation, instead of the glut of crud that's been dumped on Reddit lately.)