Page 1 of 1

Metaprogramming Academy: Templating forward declarations

Posted: January 28th, 2018, 12:14 pm
by cyboryxmen
Imagine a scenario where you have two collidable types: Circle and Square. Circle can collide with other circles and Square can collide with other squares. They can also collide with each other but implementing this would create a circular dependency. To work around this, you can forward declare Circle in Square's header and forward declare Square in Circle's header.

Square.h

Code: Select all

class Circle;

class Square
{
public:
	Square ( ) = default;
	Square ( const float side_length ) noexcept;

	bool is_colliding_with ( const Square& ) const noexcept;
	bool is_colliding_with ( const Circle& ) const noexcept;

private:
	float side_length_ { };
};
Circle.h

Code: Select all

class Square;

class Circle
{
public:
	Circle ( ) = default;
	Circle ( const float side_length ) noexcept;

	bool is_colliding_with ( const Circle& ) const noexcept;
	bool is_colliding_with ( const Square& ) const noexcept;

private:
	float side_length_ { };
};
If you make other collidable types that can collide with these, you have to forward declare those too. As you add more and more types, it quickly becomes a copypasta fest. With templates however, you can very easily automate this process.

Code: Select all

template<typename Collidable>
bool is_colliding_with ( const Collidable& ) const noexcept;
People mostly think that the definitions of class and function templates must be written in the same place as the template declaration. That's mostly true unless you're planning to define the specific instantiation of that template somewhere else. When the function template above is instantiated with a type, it becomes a forward declaration of that function of the type instantiated.

This is what happens when the function template is instantiated with Collidable == Square:

Code: Select all

bool is_colliding_with<Square> ( const Square& ) const noexcept;
As you can see, this automates the forward declaration of the is_colliding_with() function for Square for you. Now you have the ability to define this specific instantiation of the function template using template<>.

Code: Select all

bool check_collision ( const Circle&, const Square& );

template<>
bool Square::is_colliding_with<Circle> ( const Circle& circle ) const noexcept
{
	check_collision ( circle, *this );
}

template<>
bool Circle::is_colliding_with<Square> ( const Square& square ) const noexcept
{
	check_collision ( *this, square );
}
This is a trivial example to how convenient this technique is. Since it's being used for a limited list of types though, this is something that can already do easily manually anyway. Let me show you an example as to how it helps make code more generic.

Here's a function that takes an object and converts it into a string.

Code: Select all

template<typename Type>
std::string to_string ( const Type& object ) noexcept;
This function will be used by a bunch of places in your code that does output. Since it is templated, it can be used by any type you want! All you have to do is define it for the types you're using in some .cpp.

Vector.h

Code: Select all

struct Vector
{
	float x;
	float y;
	float z;
};

template<>
std::string to_string<Vector> ( const Vector& object ) noexcept
{
	return std::string () + "[ " + to_string ( object.x ) + ", " + to_string ( object.y ) + ", " + to_string ( object.z ) + " ]";
}
It's a simple concept but makes your code so much more generic and easier to develop. The std library's to_string would be customisable if it is a function template.