Enabling intellisense with templates using traits

The Partridge Family were neither partridges nor a family. Discuss.
Post Reply
User avatar
cyboryxmen
Posts: 190
Joined: November 14th, 2014, 2:03 am

Enabling intellisense with templates using traits

Post by cyboryxmen » February 20th, 2018, 12:41 pm

Templates are really useful at customising your class' behaviour. You can easily change how a template class behaves by giving the template different type parameters.

Code: Select all

template<typename InputManager>
class Keyboard
{
public:
	void Update ( InputManager& input_manager )
	{
		// Receive key presses

		while ( !keys.empty ( ) )
		{
			auto key = keys.front ( );
			input_manager.on_key_pressed ( key );
			keys.pop ( );
		}
	}

private:
	std::queue<char> keys;
};
However, as some of you who have used templates before would know, this'll prevent intellisense from working with the type InputManager. Because InputManager could be any type in the program(including another Keyboard!), intellisense can't get information on what members the type would have. Without a actual concrete 'interface', intellisense simply won't be able to provide useful information on InputManager.

However, if you look at any standard library implementation, they usually don't operate on the template type parameters directly. Most of the time, they'll do operations on them through a 'trait' class.

Code: Select all

template<typename pointer>
pointer const_iterator::operator-> ( ) const
{	// return pointer to class object
	return ( pointer_traits<pointer>::pointer_to ( **this ) );
}
In this example, pointer_traits is a trait class used to handle pointer like types. Here, it is used to get a pointer from the iterator.

Traits are simple objects used to provide information on other types. This information can help determine the implementation of a class and how its algorithms will handle these types. Most of you would have likely been introduced to them through std::numeric_limits.

Code: Select all

const auto max_int = std::numeric_limits<int>::max ( );
const auto max_float = std::numeric_limits<float>::max ( );
const auto max_double = std::numeric_limits<double>::max ( );
Using std::numeric_limits, you can tell that they're useful at providing a consistent interface to analyse a type no matter what the type actually is. This is convenient since we can use the same syntax to analyse all sorts of types making them a useful pattern in template metaprogramming. Furthermore, since they have a defined interface, you can use intellisense with them!

Code: Select all

template<typename InputManager>
// Provides intellisense with an interface to work with
struct InputManagerTraits
{
	static void on_key_pressed ( InputManager& input_manager, const char key )
	{
		input_manager.on_key_pressed ( key );
	}
}

template<typename InputManager>
class Keyboard
{
public:
	using MyInputManagerTraits = InputManagerTraits<InputManager>;
	void Update ( InputManager& input_manager )
	{
		// Receive key presses

		while ( !keys.empty ( ) )
		{
			auto key = keys.front ( );
			MyInputManagerTraits::on_key_pressed ( input_manager, key );
			keys.pop ( );
		}
	}

private:
	std::queue<char> keys;
};
Traits are good at enforcing 'concepts'. In your codebase, you can have a concept of an InputManager that has the member function on_key_pressed() that receives a key and processes it. This concept can then be represented in code through a trait class which can then be included into multiple files that use the InputManager concept. This way, you can enforce a unified interface to handle any InputManager like type accross your codebase!

Traits can also extend these types by giving them functions and variables they don't have. This is a great way of implementing 'default settings' for your concept. Imagine if your InputManager concept has a type alias called Key that sets what the key it processes is. The actual InputManager types can either explicitly say what the Key is or can just leave it to the trait class to specify. The InputManagerTraits would simply default Key to char if the InputManager type it is given does not have a Key.

Code: Select all

template<typename InputManager, typename = void>
struct GetKeyType
{	// provide fallback if Type has no Key
	using type = char;
};

template<typename InputManager>
struct GetKeyType<InputManager, std::void_t<typename InputManager::Key>>
{	// get Type::Key
	using type = typename InputManager::Key;
};

template<typename InputManager>
struct InputManagerTraits
{
	using Key = typename GetKeyType<InputManager>::type;

	static void on_key_pressed ( InputManager& input_manager, const char key )
	{
		input_manager.on_key_pressed ( key );
	}
};
That's not all though. With template specialisation, you can make a specialisation of InputManagerTraits to `bind` an existing class to its interface.

Code: Select all

class Player
{
public:
	void MoveUp ( )
	{
		// Move up
	}
	void MoveDown ( )
	{
		// Move down
	}
	void MoveLeft ( )
	{
		// Move left
	}
	void MoveRight ( )
	{
		// Move right
	}
};

template<>
struct InputManagerTraits<Player>
{
	using Key = char;

	static void on_key_pressed ( Player& player, const char key )
	{
		if ( key == 'W' )
		{
			player.MoveUp ( );
		}
		else if ( key == 'S' )
		{
			player.MoveDown ( );
		}
		else if ( key == 'A' )
		{
			player.MoveLeft ( );
		}
		else if ( key == 'D' )
		{
			player.MoveRight ( );
		}
	}
};
And just like that, Player is adapted into an InputManager. No need for inheritance nor composition!

I hope that this is enough to motivate you to use traits. Once you take a look at more metaprogramming code, you're going to see traits everywhere. Just jump into Visual Studio's implementation of the stl library and spot the places where they're used.
Zekilk

User avatar
Zedtho
Posts: 189
Joined: February 14th, 2017, 7:32 pm

Re: Enabling intellisense with templates using traits

Post by Zedtho » February 20th, 2018, 6:47 pm

Lemme comment here so that I remember to read this when I'm finally at that bit in the tutorial.

Thanks for making an effort to tell us about this stuff!

Post Reply