Page 1 of 1

std::variant again?

Posted: May 4th, 2020, 9:35 am
by albinopapa

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.

Re: std::variant again?

Posted: May 28th, 2020, 10:36 am
by Zedtho_
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?

Re: std::variant again?

Posted: May 28th, 2020, 4:33 pm
by albinopapa
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.

Re: std::variant again?

Posted: May 28th, 2020, 5:08 pm
by Zedtho_
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.

Re: std::variant again?

Posted: May 29th, 2020, 5:00 am
by albinopapa
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.

Re: std::variant again?

Posted: May 29th, 2020, 11:53 am
by Zedtho_
Thanks for the quick explanation of nested/derived classes! That's really interesting, I'll definitely be checking that out for the program!