Playing with templates and type traits

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

Re: Playing with templates and type traits

Post by albinopapa » September 18th, 2018, 4:52 pm

Haven't tested this out yet, but apparently I was incorrectly understanding how std::visit worked.

This was my understanding, visit took A callable and A variant.

Code: Select all

template<typename T, typename Callable>
void visit_binary( T&& _variant1, T&& _variant2, Callable&& _callable )
{
   std::visit( [ & ]( auto& v1 )
      {
         std::visit( [ & ]( auto& v2 )
            {
               _callable( v1, v2 );
            }, _variant2 );
      }, _variant1 );
}
After reading the documentation and some other resources on the internet, I learned that the visit function takes A callable and A LIST of variants, therefore the code above can be simplified.

Code: Select all

template<typename T, typename U, typename Callable>
void visit_binary( T&& _variant1, U&& _variant2, Callable&& _callable )
{
     auto visitor = [ & ]( auto&& v1, auto&& v2 )
          {
               using type1 = decltype( v1 );
               using type2 = decltype( v2 );
               _callable( std::forward<type1>( v1 ), std::forward<type2>( v2 ) );
          };

     // The first parameter is the "visitor" which is some callable object.
     // The rest of the parameters can be a list of variants.
     std::visit( visitor, std::forward<T>( _variant1 ), std::forward<U>( _variant2 ) );
}
That means, I could have wrote:

Code: Select all

void CheckCollisions( std::vector<std::variant<Player, Enemy>>& _variants )
{
   for( size_t i = 0; i < _variants.size(); ++i )
   {
      for( size_t j = i + 1; j < _variants.size(); ++i )
      {
         if( std::visit( Collidable::IsColliding, _variants[ i ], _variants[ j ] ) )
         {
            _obj1.TakeDamage( _obj2.DealDamage() );
             _obj2.TakeDamage( _obj1.DealDamage() );
         }
      }
   }
}
std::visit will forward any return from the callable object passed in provided that the return value for each possible invocation of that callable object returns the same value. In this case, all the Collidable::IsColliding functions return bool. The other thing about std::visit, is your callable must accept any type or combinations of types listed for each variant. In this case, that's why there are four different functions in Collidable. If there were 20 different shapes, you'd need a function for each shape and because this calls it on two variants, each function would need to be able to match a combination of the two. So in the case of 20 shapes in your variant, each shape would need 20 functions that matched, so a total of 400 functions. In the example code, we had two shapes, so we needed four functions. If we added a third, we'd need to add another five functions for a total of 9 possible matches.

That is of course for times you need to know each type, but I believe you still can have polymorphic behavior, so times you don't need to know the exact type, use a base class reference in the function signature.

Just don't forget your const correctness lol

Code: Select all

void       DoSomethingToBaseRefs(       Base& base1,       Base& base2 );
void DoSomethingToBase1WithBase2(       Base& base1, const Base& base2 );
void DoSomethingWithBase1ToBase2( const Base& base1,       Base& base2 );
void     DoSomethingWithBaseRefs( const Base& base1, const Base& base2 );
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

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

Re: Playing with templates and type traits

Post by albinopapa » September 18th, 2018, 7:55 pm

I may have a way around the 20x20 solution though, at least a way of narrowing down the possible combinations needed.

I have a list of Entity types:

Code: Select all

class HeroMale;
class HeroFemale;
class StreetThugMale;
class StreetThugFemale;
class Level1Boss;
class Level2Boss;
class Level3Boss;
class Level4Boss;
class Crate;
class Wall;
class Bullet;
class Shell;
class Grenade;
class PlasmaBall;
class KnifeEntity;
class BatEntity;
class PipeEntity;
So my entity variant would be:

Code: Select all

using entity_t =
	std::variant<
		HeroMale,
		HeroFemale,
		StreetThugMale,
		StreetThugFemale,
		Level1Boss,
		Level2Boss,
		Level3Boss,
		Level4Boss,
		Wall,
		Crate,
		Bullet,
		Shell,
		Grenade,
		PlasmaBall,
		KnifeEntity,
		BatEntity,
		PipeEntity
	>;
There are 17 types. Instead of trying to make 17x17=289 combinations of functions, we can narrow it down depending on the situation.
For the collision checking, we get the bounding object, this way we only need a few functions for them.
But let's say we only want to check collision between specific types, like HeroMale and Wall, but not Bullet and Shell.
If we leverage the type system, we can use if constexpr and a few type traits.

Remember our code from earlier:

Code: Select all

void CheckCollisions( std::vector<std::variant<Player, Enemy>>& _variants )
{
   for( size_t i = 0; i < _variants.size(); ++i )
   {
      for( size_t j = i + 1; j < _variants.size(); ++i )
      {
         if( std::visit( Collidable::IsColliding, _variants[ i ], _variants[ j ] ) )
         {
            _obj1.TakeDamage( _obj2.DealDamage() );
             _obj2.TakeDamage( _obj1.DealDamage() );
         }
      }
   }
}
Let's modify it:

Code: Select all

// We can setup a type trait to help us
template<typename T> struct is_ammo : std::false_type{};
template<> struct is_ammo<Bullet> : std::true_type{};
template<> struct is_ammo<Shell> : std::true_type{};
template<> struct is_ammo<Grenade> : std::true_type{};
template<> struct is_ammo<PlasmaBall> : std::true_type{};
template<typename T> constexpr bool is_ammo_v = is_ammo<T>::value;

template<typename T> struct is_input_controlled 
    : std::conditional_t<std::disjunction<std::is_same_v<T,HeroMale>,std::is_same_v<T,HeroFemale>>,
        std::true_type : // if T is HeroMale or HeroFemale
        std::false_type>{};  // If T is not 

template<typename T> struct is_input_constrolled_v = is_input_controlled<T>::value;

template<typename T> struct is_movable 
{
   static constexpr bool value = std::is_same_v<T::move_type, dynamic_tag>;
};

template<typename T> constexpr bool is_movable_v = is_movable<T>::value;

template<typename T, typename U> struct is_collidable_with
{
    static constexpr bool value = !std::conjunction_v<is_ammo_v<T>, is_ammo_v<U>>;
};

template<typename T, typename U> constexpr bool is_collidable_with_v = is_collidable_with<T,U>::value;

void CollisionSystem::CheckCollisions()
{
   for( size_t i = 0; i < _variants.size(); ++i )
   {
      for( size_t j = i + 1; j < _variants.size(); ++i )
      {
         auto check = [&]( auto&& obj1, auto&& obj2 )
         {
            // First, decltype gets the actual type
            // decay_t removes referenc from the type, so if auto&& obj1 turns out to be HeroMale&
            // then decay_t will return HeroMale
            // This is important in determing if two types are the same, because HeroMale and HeroMale& aren't
            using type1 = std::decay_t<decltype(obj1)>;
            using type2 = std::decay_t<decltype(obj2)>;

            if constexpr( is_collidable_with_v<type1, type2> )
            {
               // There, now if both are listed as ammo types, this function will be empty and the loops will continue
               // We eliminated the need for 16 of the 289 functions lol

               // Now we call the GetBounding functions on the members to eliminate all but the four IsColliding functions
               if( Collidable::IsColliding( obj1.GetBounding(), obj2.GetBounding() )
               {
                  // Handle collisions
                  // ...

                  // Send notification that there was a collision.
                  message_system.send<Collide>( obj1, obj2  );

                  // This too can be broken up, since something like a wall or crate won't have velocity, you can overload
                  // the MessageDispatch::send function so that one accepts a movable and non-movable object or two 
                  // movable objects.  

                  if constexpr( is_movable_v<type1> )
                  {
                     message_system.send<Collide>( obj1, obj2 );
                  }
                  else if constexpr( is_movable_v<type2> && !is_movable<type1> )
                  {
                     message_system.send<Collide>( obj2, obj1 );
                  }
               }
            }
         };

         std::visit( check, variants[i], variants[j] );
      }
   }
}
Alternatively, each type can have traits built in that can help. As shown above, is_movable expected T to have a tag of move_type. Just make sure the alias is public.

Code: Select all

struct dynamic_tag;
struct static_tag;
class Wall
{
public:
    using move_type = static_tag;
};
class Bullet
{
public:
    using move_type = dynamic_tag;
};
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

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

Re: Playing with templates and type traits

Post by albinopapa » September 18th, 2018, 10:44 pm

Ok, so I might have been a little misleading in my statement about templates not needing the definitions.

There are apparently some exceptions, namely when the compiler calls intrinsic functions or when you want to instantiate a template class.

So forward declaring all those classes and then trying to make aliases to variants of those classes doesn't work. Didn't think this would be a problem since I was only making aliases, but the STL apparently needs to know if something is_move_constructable even when the object isn't being instantiated per se. std::trivial et al, calls on the intrinsic compiler functions ( I'm guessing functions that call on outside functions about the type system ) to determine if something is movable or copyable.

Without reflection built into the language, calling these intrinsic functions is the only way to determine if a type is movable or no_throw_movable. If there was reflection, the STL would probably be able to determine if a type was movable just by looking for a missing move constructor or drill down each member of the type and see if any of them have a user defined move constructor.

Something like is_pod would be trivial.

Otherwise, each type would have to have something like:

Code: Select all

class Vec2f
{
public:
    static constexpr bool has_default_ctor = false;
    static constexpr bool has_copy_ctor = false;
    static constexpr bool has_move_ctor = false;
    static constexpr bool has_user_defined_ctors = true;
    static constexpr bool has_copy_assign = false;
    static constexpr bool has_move_assign = false;
    static constexpr bool compiler_gen_default_ctor = true;
    static constexpr bool compiler_gen_copy_ctor = true;
    static constexpr bool compiler_gen_move_ctor = true;
    static constexpr bool compiler_gen_copy_assign = true;
    static constexpr bool compiler_gen_move_assign = true;

    Vec2f()=default;
    Vec2f( float _x, float _y );
    float x, y;
};

template<typename T>struct is_default_constructable
{
    static constexpr bool value = T::has_default_ctor || T::compiler_gen_default_ctor;
};

That would be a pain in the ass.
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

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

Re: Playing with templates and type traits

Post by albinopapa » September 19th, 2018, 7:47 pm

My issue was realized after hours of exhaustion. It was a circular dependency and forward declaration problem. Aliasing the types, such as entity_t wasn't the issue. I don't know why I didn't realize this before, but types aren't checked until they are instantiated, and as long as the types used are fully defined at the point of instantiation the template class/function will work.

This does become difficult at times even with templates. Normally for something like an ECS system, you'd have base classes or interface classes that you refer to instead of the implementation classes, then just cast the base to the appropriate type if needed. However, in my attempt to use std::variant where everything needs to be known ahead of time and am trying to avoid the use of base classes other than to not have to repeat code, I'm having to get creative.

In another project, I got around this type of thing by putting everything in one file lol.

LargeHeader.h

Code: Select all

// Forward declarations
// 50+ enums classes, classes and structs forward declared or declared in the case of enum classes

// Declarations
// Fully declared each of the classes

// Definitions
// inline function definitions for each of the declared classes
This way there was no circular dependencies, and as long as I wasn't storing a concrete type to any forward declared classes, everything worked. I don't like having to change things in it, it takes a while to scroll around looking for the things I need.

I am doing something similar, though just not in one large file, but in separate files. I have a file called Declarations.h. This file has a list of headers that store the forward declarations of all the types: Entities.h, Components.h, Observers.h, etc. The Declarations.h header also defines the aliases such as entity_t, component_t and such, so I don't have to type out the full list of variants each time I want to create or pass one of the variants around.
Then as mentioned, I have the forward declaration headers.
Then I have the implementation headers which #include Declarations.h mostly for the aliases.
Then the implementation source files which can include as many headers as they need/want.

This mostly works, though I did run into the issue of entities needing access to the fully declared headers of the component_t list and weapon_t list since I was storing a vector of them.
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

User avatar
chili
Site Admin
Posts: 3948
Joined: December 31st, 2011, 4:53 pm
Location: Japan
Contact:

Re: Playing with templates and type traits

Post by chili » September 21st, 2018, 2:40 pm

Hmmm, I wasn't aware the variant visiting has multiple dispatch functionality. I would only really think of using it in a basic visitor pattern situation anyways... I'll have to look into it deeper sometime.

I've never had much need for variant or any as of yet. If managed to squeeze optional in a couple of times, but nothing where I would have said it's a game changer.
Chili

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

Re: Playing with templates and type traits

Post by albinopapa » September 22nd, 2018, 4:15 am

Honetly, the multiple variant thing I haven't tried yet, so I don't know if/how it works. I was just looking at the docs and the header file. Your wording is a bit confusing to me, so I'm going to specify that it's one visitor multiple variants of the same type I believe.


With that out of the way, std::variant is a FUCKING BITCH. It's so damn picky with what it accepts. Everything in the list of types basically has to have the same interface or write a lot of helpers to get at the underlying data.

One of the public methods is std::variant::index() which returns the index of the type that is 'active', but options are limited.
  • the list of types all have the same interface and use std::visit,
  • use 'if constexpr' to hint to the compiler which functions to use, or
  • use std::holds_alternative() and std::get to get the underlying 'active' data.
  • use the value return by std::variant::index() in a normal if/else if or switch/case block comparing the index to constants.
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

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

Re: Playing with templates and type traits

Post by albinopapa » September 22nd, 2018, 4:56 am

Now that I finally got this P.O.S. compiling, and want to test some shit out, I realized a HUGE flaw in my design.

I'm storing everything in std::vector for cache locality, GREAT!
My Entity class store their components in an std::vector, FANTASTIC!
My messages are passed by value and stored in a vector for each derived Receiver, ALRIGHT!
My base System class stores entities in a static std::vector, GRE..wait what?
My derived system classes store pointers to entities stored in the parent System class, OH, SHIT!

These pointers are raw pointers to the entities in the System::std::vector<entity_t> entities vector, so if the vector grows and has to reallocate, guess what? All those pointers are useless.

So what are my options? Just to clarify, most of the ECS designs I've seen using heap allocated objects using raw malloc/free( C ), new/delete( pre-C++11 ), std::unique_ptr or std::shared_ptr( since C++11 ). The biggest advantages of this approach is polymorphism and stable pointers. With heap allocation, the pointers always SHOULD point to the same address regardless if your vector reallocates. The biggest disadvantages that comes to mind is determining the derived types, most use size_t and some form of bit manipulation or even enum/enum class types and casting if two enums for flags match

So I thought to myself, wow, std::variant would be an awesome way to implement an ECS system since the type system will help out and you get cache locality by storing your types in a vector of variants. You also get static and dynamic polymphism using the type system for static polymorphism and base classes for dynamic polymorphism. Seems like a win win right? Well, it depends on the situation. See, std::variant can't hold pointers, so in the case where you'd want to share entities between Systems or other parts of the program,
  • I either have to make a request for the entities ( which means iterating through a vector to find entities that match some criteria for each system )
  • I could make copies of the different variants for each System ( which sounds like a definite no, since I'd still need to maintain coherence between the copies
  • I could probably get away with an std::variant of shared_ptr or unique_ptr objects, but then that would defeat the goal of cache locality.
  • I could iterate through the whole list of entities, branch on which entities meet requirements for each derived class, send a message to each class forwarding the entity's address. This means that the pointers will be valid at least until the next frame, which is long enough to process all the entity pointers in the other classes.
  • I could do the same as previous, but instead of passing pointers, I'd pass copies the entities. The ones that need to pass to multiple systems would have to be forwarded to the next system of course.
I think the last one has the best chance to allow for multi-threading if that is something I ever get into using a thread pool or something.

Any other ideas?
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

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

Re: Playing with templates and type traits

Post by albinopapa » October 5th, 2018, 6:52 am

OMG! The price of admission for templates, variants, variants as template parameters?

This is a World class variant with just 2 system variants as template parameters. Explanation of woes to follow.

Code: Select all

using world_t = std::variant<
	screws::ECS_World<
	std::variant<
		screws::ECS_System<
			MovableDispatcher,
			MovableMessageHandler,
			std::variant<
				screws::ECS_Entity<player_tag, component_t, std::variant<
					screws::ECS_Message<componentAdded_tag>,
					screws::ECS_Message<componentRemoved_tag>,
					screws::ECS_Message<entityAdded_tag>,
					screws::ECS_Message<entityRemoved_tag>,
					screws::ECS_Message<systemAdded_tag>,
					screws::ECS_Message<systemRemoved_tag>>>,
				screws::ECS_Entity<enemy_tag, component_t, std::variant<
					screws::ECS_Message<componentAdded_tag>,
					screws::ECS_Message<componentRemoved_tag>,
					screws::ECS_Message<entityAdded_tag>,
					screws::ECS_Message<entityRemoved_tag>,
					screws::ECS_Message<systemAdded_tag>,
					screws::ECS_Message<systemRemoved_tag>>>>,
			std::variant<
				screws::ECS_Message<componentAdded_tag>,
				screws::ECS_Message<componentRemoved_tag>,
				screws::ECS_Message<entityAdded_tag>,
				screws::ECS_Message<entityRemoved_tag>,
				screws::ECS_Message<systemAdded_tag>,
				screws::ECS_Message<systemRemoved_tag>>,
			SystemMessageFilter>,
		screws::ECS_System<
			DrawableDispatcher,
			DrawableMessageHandler,
			std::variant<
				screws::ECS_Entity<player_tag, component_t, std::variant<
					screws::ECS_Message<componentAdded_tag>,
					screws::ECS_Message<componentRemoved_tag>,
					screws::ECS_Message<entityAdded_tag>,
					screws::ECS_Message<entityRemoved_tag>,
					screws::ECS_Message<systemAdded_tag>,
					screws::ECS_Message<systemRemoved_tag>>>,
				screws::ECS_Entity<enemy_tag, component_t, std::variant<
					screws::ECS_Message<componentAdded_tag>,
					screws::ECS_Message<componentRemoved_tag>,
					screws::ECS_Message<entityAdded_tag>,
					screws::ECS_Message<entityRemoved_tag>,
					screws::ECS_Message<systemAdded_tag>,
					screws::ECS_Message<systemRemoved_tag>>>>,
			std::variant<
				screws::ECS_Message<componentAdded_tag>,
				screws::ECS_Message<componentRemoved_tag>,
				screws::ECS_Message<entityAdded_tag>,
				screws::ECS_Message<entityRemoved_tag>,
				screws::ECS_Message<systemAdded_tag>,
				screws::ECS_Message<systemRemoved_tag>>,
				SystemMessageFilter>>,
			std::variant<
				screws::ECS_Message<componentAdded_tag>,
				screws::ECS_Message<componentRemoved_tag>,
				screws::ECS_Message<entityAdded_tag>,
				screws::ECS_Message<entityRemoved_tag>,
				screws::ECS_Message<systemAdded_tag>,
				screws::ECS_Message<systemRemoved_tag>>,
	WorldMessageFilter>>;
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

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

Re: Playing with templates and type traits

Post by albinopapa » October 5th, 2018, 7:00 am

I'm sure there is a way to break this behemoth down, but as it stands, this at least allows the program to compile. I'm afraid aliases don't work well in this case. I was having issues with some of the other variants, because the systems needed message_t ( variant of messages ) and the message classes needed system_t ( variant of systems ).

VS didn't like me putting the alias before the declaration of the message classes, cause variant likes/needs fully formed types. It also didn't like me putting it after the declaration, cause then the definitions for the message classes couldn't find the alias. Declarations was in header, definitions were in cpp file.

I ended up having to put all code in one file, and move stuff around until it compiled. The declarations are in one file that is, while the definitions are all in separate source files.

This particular massive declaration can probably use the aliases because everything it depends on are fully declared at that point, just wanted to show the mess first :).
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

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

Re: Playing with templates and type traits

Post by albinopapa » October 7th, 2018, 6:39 pm

Hooray!!!

I was able to find a way to use some aliases and even forward declarations.

My World class variant now looks like this:
using World = screws::ECS_World<system_t, message_t, WorldMessageFilter>;
using world_t = std::variant<World>;

Something that I was having troubles with was std::variant and incomplete types, so after some serious thought, I decided to create tags, which are just empty structs.

Code: Select all

struct componentAdded_tag {};
struct componentRemoved_tag {};
struct entityAdded_tag {};
struct entityRemoved_tag {};
struct systemAdded_tag {};
struct systemRemoved_tag {};
struct key_press_tag {};
struct key_release_tag {};
struct mouse_button_press_tag {};
struct mouse_button_release_tag {};
struct mouse_wheel_scroll_tag {};
These are the message tags. Then I have an empty ECS_Message struct that takes as it's template parameter one of these tags

Code: Select all

	template<typename...MessageTypes> struct ECS_Message {};
Now I can forward declare the messages

Code: Select all

// Message aliases
using ComponentAdded = screws::ECS_Message<componentAdded_tag>;
using ComponentRemoved = screws::ECS_Message<componentRemoved_tag>;
using EntityAdded = screws::ECS_Message<entityAdded_tag>;
using EntityRemoved = screws::ECS_Message<entityRemoved_tag>;
using SystemAdded = screws::ECS_Message<systemAdded_tag>;
using SystemRemoved = screws::ECS_Message<systemRemoved_tag>;
using KeyPress = screws::ECS_Message<key_press_tag>;
using KeyRelease = screws::ECS_Message<key_release_tag>;
And then make my variant alias

Code: Select all

// Message list alias
using message_t = std::variant<
	ComponentAdded,
	ComponentRemoved,
	EntityAdded,
	EntityRemoved,
	SystemAdded,
	SystemRemoved,
	KeyPress,
	KeyRelease
>;
This allowed me to be able to use the message_t alias instead of having to list the full variant each time. It also allowed me to move the declaration and definitions into their own files.
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

Post Reply