Be mindful of the `const` qualifier when taking arguments by reference. I've had some surprises when I forgot to add `const` and overload resolution picked the default `auto &` case. This is especially subtle if this is a sub-element of a larger object. The `const` further up the ownership chain can be implicitly propagated down. It's easy to overlook this.
This is pretty useful in a tight loop which you may wanna do some configurations before the loop starts with some boolean variables which will be evaluated in the loop. At this time, the runtime checking will create a branch which may break something like pipeline. But with `std::visit` and `std::variant` you van evaluate it in compile time.
@@Rojfos The example in the video is trivial in that the return type (void) is independent of the input type. If however you have a visitor that has a return type that depends on the input type, then it gets more complicated as @gregossamsa eluded to
@@goncalogomes5036 it all depends on your use case. For mine, the visitor would have some constexpr statements to determine how to handle the different variant types depending on the stored value type and modify an array. No return type was needed. The array would either be an array of variants or some templated container that didn't care what the underlying type was. I agree in principle though that returning a variant would also be possible from a lambda.
Very nice video! Thanks a lot! I would love to see part two of this where you go into details of implicit conversion pitfalls of the overload pattern and how to deal with these! (Andreas Fertig has a great blog post on safely visiting a std::variant)
@@Evan490BC Right? I'd be okay with something like: switch( my_variant ){ case: // do something with circle case: // do something with rectangle } I did try creating an interface for something like this. template struct Switch{ }; template struct Case{ }; Can't remember how I had it set up, it's been a few years. Basically, the program would have to compare each template Case parameter. Once a match is found, it executes the given Fn within the Case struct. It does clean up some of the inline code because it can be defined somewhere else, but has some added runtime cost.
Granted you are right. Though it still shows how powerful C++ is. In what other language would you be able to implement something like this in compile time and without language level support?
these trivial examples are ok with only one line in each lambda, but i think i’d prefer reading the ‘if constexpr (std::same_as_v)’ version in a single large lambda. imo having multiple lambdas as args seems like it could be a little harder to follow/parse. I guess it’s down to personal preference.
std::same_as is a concept, I think you mean std::is_same_v. I've played around with std::variant and have come up with several ways of dealing with variants. 1) Like inheritance. struct Circle{ float area()const; }; struct Rectangle{ float area()const; }; struct Shape{ float area()const{ return std::visit( []( auto const& shape_ ){ return shape_.area(); }, m_shape_var ); } std::variant m_shape_var; }; Usage: const auto area = shape.area(); 2) Conditional std::get_if or std::holds_alternative if( Circle* ptr = std::get_if( &shape.m_shape_var );ptr ) { // do things } else if( Rectangle* ptr = std::get_if( &shape.m_shape_var ){ do other things } if( std::holds_alternative( shape.m_shape_var ) ){ // do things } else if( std::holds_alternative( shape.m_shape_var) ){ // do other things } 3) Static dispatch; using both methods listed in this video 4) if constexpr block std::visit( [&]( auto const& var_ ){ using type = std::decay_t; if constexpr( std::is_same_v ){ // I do this so intellisense ( Visual Studio ) can show what's available Circle const& circle = var_; } else if constexpr( std::is_same_v ) { Rectangle const& rectangle = var_; } }, shape.m_shape_var ); My favorite use for std::variant is definitely in place of inheritance. This means I can create a single interface ( Shape ) and hide the implementation details in the pseudo derived classes. This also reduces the need for any of the other methods for dealing with specific types.
@@MrAlbinopapa In case of 4), you can also simplify it using a lambda with an explicit template parameter list: std::visit( [&]( T const& var_ ){ if constexpr( std::is_same_v ){ // I do this so intellisense ( Visual Studio ) can show what's available Circle const& circle = var_; } else if constexpr( std::is_same_v ) { Rectangle const& rectangle = var_; } }, shape.m_shape_var );
I dont understand why this isnt the default, I can see that maybe you want to use an instance of a functor but most probably Inwould die before I would need that, instead this looks like the 99,99% use case for a visitor, but somehow we have to do this ourselves instead of a library doing it for us
Because an if-else-if chain is an if-else-if chain, potentially evaluating the condition for all branches (and thus may branch N-1 times given N branches), which are not cheap. std::visit *may* be implemented using a table of function pointers, thus requiring only an array access into said table, and calling the obtained function pointer (which in turn casts the variant storage to a reference of the contained type and calling the apropriate overload of the provided function object). This results in a single “indirect jump/call”, which is faster than an arbitrary amount of branches.
Mainly because this does exhaustiveness checking. All variant types must be covered by one of the lambdas (or call operators if you're hand-writing a visitor), or you get a compile-time error. That way, when you add a type to the variant later, the compiler will helpfully point out all the places you need to adapt in your code.
As far as I know, there is no such thing as std::overload. But on cppreference there's a code example with a struct called "overloaded" that's pretty similar to what Jason did in this video. You can find it on the page about std::visit. It would be cool if that would be standardised, though!
This one feels like more of a hacky kludge from the committee to address a problem that would've been better addressed by adding on to switches. I'm sure if someone reads this they'll claim that changing switches would potentially break existing code somewhere somehow, but if the compiler is written correctly then it's simply a matter of context switching based on what types are being cased on and it shouldn't break anything if the compilers are written the correct way.
You're moving the code for each case into separate functions, breaking the code flow. I no longer see what the print method does at a single glance, because you forced me to scour the source for all the implementations of print_value.
@@norton7715 Even if you don't I would argue it's still nicer to use the clever fancy "visitor" multi-operator CTAD object with the lambdas, and _then_ delegate to print_int, print_float and print_string. Precisely for the reasons @vytah invokes: you get to see the dispatching table in one place.
can this code style be anymore obscure and hard to read? ... drifting towards gimmicks don't you think? gimme real life problems that are better solved using this.
Screw all this noise, just check holds_alternative and act accordingly. Real life uses of std::variant aren't for integers and floats. A common use I see is for UI elements like buttons and windows and menus, where you have a sender and a receiver and the receiver needs to act according to it's sender. A generic lambda setup like this would just obfuscate and clutter the code.
The fact that you can multiple-inherit from lambda types... C++ is so wild.
Ikr, every time I watch these videos, I learn something new about C++
since lambda desugar to a struct with operator() it's less "magical" (the syntax still looks like it thought).
@@miniropso it's like functional interface in java
Thanks again Jason for explaining complex concepts in a very simple, understandable way !
Be mindful of the `const` qualifier when taking arguments by reference. I've had some surprises when I forgot to add `const` and overload resolution picked the default `auto &` case. This is especially subtle if this is a sub-element of a larger object. The `const` further up the ownership chain can be implicitly propagated down. It's easy to overlook this.
This is pretty useful in a tight loop which you may wanna do some configurations before the loop starts with some boolean variables which will be evaluated in the loop. At this time, the runtime checking will create a branch which may break something like pipeline. But with `std::visit` and `std::variant` you van evaluate it in compile time.
Note that std::visit requires return types to match for all covered types. This becomes a pain because you must leak abstractions down.
Could somebody please explain?❤
@@Rojfos The example in the video is trivial in that the return type (void) is independent of the input type. If however you have a visitor that has a return type that depends on the input type, then it gets more complicated as @gregossamsa eluded to
in that case can't you just return a variant (I guess you need to be explicit about that in the return of the lambdas)
@@goncalogomes5036 it all depends on your use case. For mine, the visitor would have some constexpr statements to determine how to handle the different variant types depending on the stored value type and modify an array. No return type was needed. The array would either be an array of variants or some templated container that didn't care what the underlying type was.
I agree in principle though that returning a variant would also be possible from a lambda.
you just return another variant )
Very nice video! Thanks a lot! I would love to see part two of this where you go into details of implicit conversion pitfalls of the overload pattern and how to deal with these! (Andreas Fertig has a great blog post on safely visiting a std::variant)
Why is there no standard visitor class?
Wonderful example of why we need pattern matching, all the workarounds we have to do because we do not have it are just painful
My thought exactly! Bring the bloody pattern matching into C++ already!
@@Evan490BC Right? I'd be okay with something like:
switch( my_variant ){
case: // do something with circle
case: // do something with rectangle
}
I did try creating an interface for something like this.
template struct Switch{
};
template struct Case{
};
Can't remember how I had it set up, it's been a few years. Basically, the program would have to compare each template Case parameter. Once a match is found, it executes the given Fn within the Case struct. It does clean up some of the inline code because it can be defined somewhere else, but has some added runtime cost.
Damn this is crazy! I've never seen this sort of magic. Thanks for letting us know about this. Though I'm not a big fan of visitor.
0:46 Jason, no doubt is a constexpr god.
Pattern matching, but obtuse and not directly supported by the compiler
Granted you are right. Though it still shows how powerful C++ is. In what other language would you be able to implement something like this in compile time and without language level support?
❤
This is crazy, I thought the current way of doing it was a chain of
if constexpr (std::same_as)
these trivial examples are ok with only one line in each lambda, but i think i’d prefer reading the ‘if constexpr (std::same_as_v)’ version in a single large lambda. imo having multiple lambdas as args seems like it could be a little harder to follow/parse. I guess it’s down to personal preference.
std::same_as is a concept, I think you mean std::is_same_v.
I've played around with std::variant and have come up with several ways of dealing with variants.
1) Like inheritance.
struct Circle{
float area()const;
};
struct Rectangle{
float area()const;
};
struct Shape{
float area()const{
return std::visit( []( auto const& shape_ ){
return shape_.area();
}, m_shape_var );
}
std::variant m_shape_var;
};
Usage: const auto area = shape.area();
2) Conditional std::get_if or std::holds_alternative
if( Circle* ptr = std::get_if( &shape.m_shape_var );ptr ) { // do things }
else if( Rectangle* ptr = std::get_if( &shape.m_shape_var ){ do other things }
if( std::holds_alternative( shape.m_shape_var ) ){ // do things }
else if( std::holds_alternative( shape.m_shape_var) ){ // do other things }
3) Static dispatch; using both methods listed in this video
4) if constexpr block
std::visit( [&]( auto const& var_ ){
using type = std::decay_t;
if constexpr( std::is_same_v ){
// I do this so intellisense ( Visual Studio ) can show what's available
Circle const& circle = var_;
}
else if constexpr( std::is_same_v ) {
Rectangle const& rectangle = var_;
}
}, shape.m_shape_var );
My favorite use for std::variant is definitely in place of inheritance. This means I can create a single interface ( Shape ) and hide the implementation details in the pseudo derived classes. This also reduces the need for any of the other methods for dealing with specific types.
@@MrAlbinopapa I clearly remember that concepts are "convertible" to booleans. And you can apply boolean && and || operators on them
@@MrAlbinopapa In case of 4), you can also simplify it using a lambda with an explicit template parameter list:
std::visit( [&]( T const& var_ ){
if constexpr( std::is_same_v ){
// I do this so intellisense ( Visual Studio ) can show what's available
Circle const& circle = var_;
}
else if constexpr( std::is_same_v ) {
Rectangle const& rectangle = var_;
}
}, shape.m_shape_var );
I dont understand why this isnt the default, I can see that maybe you want to use an instance of a functor but most probably Inwould die before I would need that, instead this looks like the 99,99% use case for a visitor, but somehow we have to do this ourselves instead of a library doing it for us
But why would I want to use this over std::holds_alternative in an if/else-if/else statement?
Because an if-else-if chain is an if-else-if chain, potentially evaluating the condition for all branches (and thus may branch N-1 times given N branches), which are not cheap.
std::visit *may* be implemented using a table of function pointers, thus requiring only an array access into said table, and calling the obtained function pointer (which in turn casts the variant storage to a reference of the contained type and calling the apropriate overload of the provided function object).
This results in a single “indirect jump/call”, which is faster than an arbitrary amount of branches.
Mainly because this does exhaustiveness checking. All variant types must be covered by one of the lambdas (or call operators if you're hand-writing a visitor), or you get a compile-time error.
That way, when you add a type to the variant later, the compiler will helpfully point out all the places you need to adapt in your code.
Didn't we also get std::overload to do the exact same thing as the 'visitor' template struct you made?
As far as I know, there is no such thing as std::overload. But on cppreference there's a code example with a struct called "overloaded" that's pretty similar to what Jason did in this video. You can find it on the page about std::visit. It would be cool if that would be standardised, though!
very neat implementation. i still want to have a generalized pattern matching if possible in the standard though.
This one feels like more of a hacky kludge from the committee to address a problem that would've been better addressed by adding on to switches. I'm sure if someone reads this they'll claim that changing switches would potentially break existing code somewhere somehow, but if the compiler is written correctly then it's simply a matter of context switching based on what types are being cased on and it shouldn't break anything if the compilers are written the correct way.
I wouldn't look at this as a kludge from the committee - it's a thing that users came up with.
@@cppweekly It's in the standard though, which means they approved it.
This is not working for me on c++20 clang, anyone else got any idea?
Depends on the version of clang, but it was slow implementing this feature.
I don't understand why you always shoehorn the Clion adverts in the middle of a sentence. Nobody else does that.
int main() {
auto print = [](auto y){std::visit([](auto x){
struct print {
print (int i) { printf("%i
",i);}
print (float f) { printf("%f
",f);}
print (std::string_view sv) { printf("%.*s
", static_cast(sv.size()), sv.data()); }
} print(x);
}, y);};
std::variant var = 123;
print(var);
var = "Hello World!";
print(var);
}
aspirationally:
int main() {
std::visit( [](auto val){ std::cerr
ruclips.net/video/et1fjd8X1ho/видео.html what can be also static starting from C++23?
the call operator ('operator( )') can now be static
Why is std::print a pile of crap?
it's a new feature, it's gonna be good eventually
void print_value(int i) {
fmt::print("Int: {}
", i);
}
void print_value(float f) {
fmt::print("Float: {}
", f);
}
void print_value(std::string_view sv) {
fmt::print("SV: {}
", sv);
}
void print(value_t value) {
std::visit([](auto value){ print_value(value); }, value);
}
int main() {
const auto value = get_variant();
print(value);
}
You're moving the code for each case into separate functions, breaking the code flow. I no longer see what the print method does at a single glance, because you forced me to scour the source for all the implementations of print_value.
@@vytah yes if you only write slideware then jason's version is better
@@norton7715 Even if you don't I would argue it's still nicer to use the clever fancy "visitor" multi-operator CTAD object with the lambdas, and _then_ delegate to print_int, print_float and print_string. Precisely for the reasons @vytah invokes: you get to see the dispatching table in one place.
@@tbkih the code i posted does exactly what you are describing
"Breda" has the stress on the 2nd syllable. "The Netherlands" has the definite article as part of the name.
Why would he know about that
can this code style be anymore obscure and hard to read? ... drifting towards gimmicks don't you think? gimme real life problems that are better solved using this.
You can have a method returning a std::variant, so the visitor MUST handle all possible returned types, some sort of pattern matching.
Skill issue
Screw all this noise, just check holds_alternative and act accordingly. Real life uses of std::variant aren't for integers and floats. A common use I see is for UI elements like buttons and windows and menus, where you have a sender and a receiver and the receiver needs to act according to it's sender. A generic lambda setup like this would just obfuscate and clutter the code.
C++ has become an abominable language
Became, like over 30yrs ago from Oct 1991
Then why are you watching the video?
@@WilhelmDrake Coz' I'm a C++ dev, but I just don't like the language as much as before.