std::variant again?

The Partridge Family were neither partridges nor a family. Discuss.
Post Reply
albinopapa
Posts: 4145
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

std::variant again?

Post by albinopapa » May 4th, 2020, 9:35 am

Code: Select all

#include <iostream>
#include <variant>

void do_int( int ) { std::cout << "do_int chosen\n"; }

using TestVar = std::variant<int, double, float>;

template<typename T>
class Switch
{
public:
	Switch( T& value )
		:
		var( value )
	{}

	template<typename MatchType, typename CaseBlock, typename...Args>
	Switch& Case( CaseBlock block, Args&&...args )
	{
		auto memberfn_call = [ & ]( auto& var_, auto& object, auto&&...args_ )
		{
			( object.*block )( var_ );
		};

		std::visit( [&]( auto& var_ )
		{
			using type = std::decay_t<decltype( var_ )>;
			if constexpr( std::is_same_v<type, MatchType> )
			{
				if constexpr( std::is_member_function_pointer_v<CaseBlock> )
				{
					memberfn_call( 
						std::forward<decltype( var_ )>( var_ ), 
						std::forward<Args>( args )... 
					);
				}
				else
				{
					block( std::forward<Args>(args)..., std::forward<type>( var_ ) );
				}
			}
		}, var );

		return *this;
	}
private:
	T& var;
};

struct ATest
{
	void operator()( double d ) { std::cout << "ATest::operator() chosen\n"; }
	void do_float( float f ) { std::cout << "ATest::do_float chosen\n"; }
};
int main()
{
	TestVar v = 42.0;

	ATest atest;
	Switch{ v }
		.Case<int>( do_int )
		.Case<double>( atest )
		.Case<float>( &ATest::do_float, atest );

	v = 420;
	Switch{ v }
		.Case<int>(do_int)
		.Case<double>( atest )
		.Case<float>( &ATest::do_float, atest );

	return 0;
}

Code: Select all

Output:
ATest::operator() chosen
do_int chosen
Just a proof of concept or concept of proof...don't care.

Works with free function pointers, member function pointers functors and lambdas.

A few weeks ago I said I wanted something like a switch statement on types:

Code: Select all

switch<type>( v )
{
    case<int>:
    case<double>:
    case<float>:
}
Well, I can't change the compiler or the standard, but I can make something that looks similar. I don't have a Default<> case figured out nor an early out mechanism if a case was triggered.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Zedtho_
Posts: 6
Joined: May 28th, 2020, 9:27 am

Re: std::variant again?

Post by Zedtho_ » May 28th, 2020, 10:36 am

Ohh man this is actually something I've been looking for myself! Thanks a lot! Could this be used for subclasses as well by chance?

albinopapa
Posts: 4145
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: std::variant again?

Post by albinopapa » May 28th, 2020, 4:33 pm

When you say subclasses, do you mean nested classes? or do you mean derived classes? or user defined classes? or something else?

I haven't done a lot of testing with this bit of code really. It was kind of a proof of concept really.

I'd like to have a way of halting execution of the other "Cases" since it's possible that the first Case matches, but it will still continue checking all others.

There are a lot of things to take into account here with this example:
  • There are not constraints on template parameters
  • There are not static or dynamic checks to ensure template parameters are valid
  • and probably more
Most of what I can think of are variations of the two things I mentioned. For instance, this is only designed to work on std::variant and there is no constraint nor testing nor specialization for std::variant and it's types. One could also validate that MatchType is actually one of the types that work with the current std::variant object. That last thing that I could think of that would need validation is to make sure the CaseBlock template argument is callable or invokeable given the arguments passed in.

In summary, this code is unfinished and is therefore unsafe to use unless it is used as intended.
Don't be fooled, while type checking is done during compilation, there's still going to be the calls to std::visit and each call to Switch::Case. While the built in switch/case block has a single jump to instruction mechanic, this does not.

If you search cppreference.com for std::visit, there is a template utility there called "overload" or "overloads" that allows you to use a list of lambda function almost in the same manner. The issue I've had using it though is my lambdas aren't always one liners so things get messy unless I'm simply using those lambdas to forward to other functions.

I have an idea that might show performance details using this code, but it will take some time to setup the tests using the different methods of using std::variant.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Zedtho_
Posts: 6
Joined: May 28th, 2020, 9:27 am

Re: std::variant again?

Post by Zedtho_ » May 28th, 2020, 5:08 pm

Ahh okay makes sense, that's really cool though!

I have to admit I am not sure I understand what you mean with
Do you mean nested classes? or do you mean derived classes? or user defined classes? or something else?
as I do not know those words haha, but I was thinking of the classic inheritance where you do something like

Code: Select all

class Monster
{
}

class Cat : public Monster
{

}
In an example I am trying right now in fact, I have three classes like this:

Code: Select all

class Agent{}

class TFT : public Agent{}
class Coordinator : public Agent{}
class Deflector : public Agent{} 
and am currently storing TFT, Coordinator and Deflector as an enum in Agent so that I can do switch cases, but it's kind of wasted data because it's obvious from the class itself what it is.

albinopapa
Posts: 4145
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: std::variant again?

Post by albinopapa » May 29th, 2020, 5:00 am

Nested class

Code: Select all

class Outer
{
public:
    class Inner
    {
    };
};

Outer::Inner inner_object;
Derived class

Code: Select all

class A{};
class B : public A{}; <- B is a derived class of A

A* pA = new B{};
A user defined is any class actually. The basic types ( char, short, int, float double ) aren't classes, which is why I thought maybe you were asking since I only used the basic types. The Switch template class "should" work with classes as well.

As far as it working with derived classes, yes and no. If you pass an A& when in reality it's a B&, it won't pick up on that. However, because of the way std::variant works, you wouldn't need to derive TFT, Cooperator and Defector from Agent. The way std::variant works is something close to:

Code: Select all

class Agent
{
public:
    Agent()=default;
    Agent( TFT const& value_ )
        :
    index( 0 ),
    tft_agent( value_ )
    {}
    Agent( Cooperator value_ )
        :
    index( 1 ),
    coop_agent( value_ )
    {}
    Agent( Defector value_ )
        :
    index( 2 ),
    dafuq_agent( value_ )
    {}

private:
    union {
        TFT tft_agent;
        Cooperator coop_agent;
        Defector dafuq_agent;
    };
    int index = -1;
};
This is very simplified and not exactly how std::variant is implemented, but the same elements are there. There is an index which keeps track of the current "active" type. When you use std::visit(), the active member is passed to the function object provided to std::visit(). It's up to the compiler to determine which function to call.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Zedtho_
Posts: 6
Joined: May 28th, 2020, 9:27 am

Re: std::variant again?

Post by Zedtho_ » May 29th, 2020, 11:53 am

Thanks for the quick explanation of nested/derived classes! That's really interesting, I'll definitely be checking that out for the program!

Post Reply