C++ Template Function Basics

This is the first article in the template series. In this article, we will discuss a few common ways to properly make use of template functions. In the future series, we will talk about other techniques such as overloading, traits, policies, and interesting patterns in Template Meta-Programming.

Template functions are good abstractions when we want to allow one implementation to process different but similar types of data without creating redundancy.

Implement Inside Header

The most common way of making template functions is to define and implement the function both in the header file. Let’s take an example from the trajectory generation module. It is as straight forward as:

template<typename TrajectoryType>  /* here, `typename` and `class` are interchangeable */
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const TrajectoryType& trajectory, double u) {
	...
}

You can further request that TrajectoryType must be a type of trajectory, by doing this:

#include <type_traits>
#include "trajectory.h"

// in addition to regular trajectory, SE3Trajectory defines a 6DoF pose
class SE3Trajectory : public Trajectory {
	...
}

template<typename TrajectoryType>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const TrajectoryType& trajectory, double u) {
	static_assert(std::is_base_of<SE3Trajectory, Trajectory>::value, 
                  "TrajectoryType must be derived from Trajectory class");
  ...
}

so that we know SE3Trajectory would work, not any other random type. This assert itself is another fancy way of using templates, which we will cover in later series.

This is the easiest way of using template functions. Template functions are not functions, but templates for code! They are only instantiated and compiled when they are needed, just like code generators executed at compile time. If you put the implementation into source file like any other regular function, the program won’t link, and you will see the error undefined reference to ... during compilation. The fundamental reason is the One Definition Rule, but we won’t go there for now.

If this is so easy, why do we need anything else? Well, problem is, with clarity and convenience, comes the burden for compilation. Every single file that includes this header file would have its own copy of the function templates and all of their definitions. It could slow down the compilation, even when the linker is sorting things smartly, because that would take time too!

Implement Inside the Source File

To avoid repeatedly injecting the templates into all files that include this header file, one can put the implementation into the source file, as regular functions. As mentioned above, this would cause compilation error.

To resolve this issue, we need to instantiate the concrete types of the template at the end of the file.

#include "trajectory.h"

template<typename TrajectoryType>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const TrajectoryType& trajectory, double u) {
	...
}

// explicity instantiation
template PathPointType GetNextGoal(
	const RobotInterface& robot_api, const SE3Trajectory& trajectory, double u);
template PathPointType GetNextGoal(
	const RobotInterface& robot_api, const SpatialVelocityTrajectory& trajectory, double u);

This could save some work for the linker: there is no need to re-implement each type, and the implementation would stay the same for all of them, which is very convenient. However, you need to know exactly the types the program are dealing with, and if there’s a new type, you would have to change the source code by adding the new template instantiation. Maybe not that good for maintenance!

Implement w/ Template Specialization

The method described above saves us a lot of effort by avoiding re-implementation for different types, but what if we do need specific modification for certain types, say SpatialVelocityTrajectory, because we cannot simply treat it like a regular trajectory? Template Specialization would come to help.

template<>
PathPointType GetNextGoal(
	const RobotInterface& robot_api, const SpatialVelocityTrajectory& trajectory, double u) {
	...
}

You can also declare it in the header file, and separately define it in the source file. There’s one catch though: do not declare the specialization in the source file!

However, this is not recommended—if we need a specific implementation for this type, why do we use template to define it? In this case, we can simply overload it by a non-template function:

PathPointType GetNextGoal(
	const RobotInterface& robot_api, const SpatialVelocityTrajectory& trajectory, double u) {
	...
}



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Partial Template Specialization
  • Trajectory Basics I: Foundations
  • LLM Study Notes III: Post-Training