Partial Template Specialization

Last time we talked about a little bit of Template Specialization at the end. Formally speaking, it is “full” or “explicit” template specialization, because we specify explicitly all of the types we need to take special care with (even though it’s just one type in our example). What if we have a template with multiple types? There comes partial template specialization for this scenario. It has not much difference from explicit template specialization, except just changing one or more parameters:

template<class T, int I, class U, int J>
struct A {};    // primary template

template<class V, int K>
struct A<V*, K, V, K * 2> {};   // partial specialization

the above example might look overwhelming, but it’s just a show-off for what partial specialization can do—you can use whatever parameter symbols, be it U, V, or K, as long as the final argument lists match; one catch is K has to appear before K * 2, because the first parameter must be deducible. There are some other catches and rules, but whew, you are never going to worry unless you intend to write code that confuses other team members.

But what can this do? Well, in reality, the above example is pretty much useless, because your team would likely be in serious trouble if something similar exists in the code base. One good example in the real world is std::unique_ptr, where it manages a dynamically-allocated array of objects by partial specialization:

template<class T, class Deleter = std::default_delete<T>>
class unique_ptr<T[], Deleter> {};

We would however not expand on this topic, because there’s a far more interesting and powerful function using partial specialization. The story follows what we’ve discussed in Template Function Basics. We’ve talked about how to ensure only allowing a specific inheritance type for a template function using std::is_base_of, and the anti-pattern of specializing for only one type. In the light of this, std::enable_if comes to rescue, and leads to more fancy possibilities. It is implemented simply and elegantly with four lines:

template<bool B, typename T = void>
struct enable_if {};     // primary template

template<typename T>
struct enable_if<true, T> { typedef T type; };    // partial specialization

What’s so special about those four lines? Because C++ empowers them with magic: SFINAE. It’s an abbreviation for “Substitution Failure Is Not An Error”, which means if the type deduction fails because of invalid substitution, it will raise no error, and the compiler will ignore this expression. This is what makes the partial specialization of enable_if useful.

Let’s start from our previous example:

template<typename TrajectoryType>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const TrajectoryType& trajectory, double u, 
	    typename enable_if<is_type_se3_trajectory<TrajectoryType>::value, TrajectoryType>::type* = 0) {
	...
}

What happens here is, if the TrajectoryType is not a type of trajectory, the value is false and the primary template is selected, which has a void type and triggering SFINAE. This function definition will then be ignored by the compiler, as opposed to what we had in the previous article, where we have to call static_assert in the function body for evaluation. The =0 part is merely to hide this argument from the user, and we don’t need a name for the arg here either. If this looks too confusing, or you don’t want to introduce a non-used argument in the function, this works as well:

template <typename T, typename = typename enable_if<is_type_se3_trajectory<T>::value>::type>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const T& trajectory, double u) {
    ...
}

// given the function definition above, adding following code would not work
template <typename V, typename = typename enable_if<is_type_so2_trajectory<V>::value>::type>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const V& trajectory, double u) {
    ...
}

Here, when is_type_se3_trajectory<T>::value is evaluated true, partial specialization template is triggered and deduces the TrajectoryType type. However, if we try to add another type check with a different implementation, say is_type_so2_trajectory, the code will fail to compile. This is because to the compiler, it doesn’t care whether it’s T or V. The two definitions here only differ in their default template arguments, so they are treated as redefinition! To soTe this, we have to make the difference appear somewhere else:

template <typename T, typename enable_if<is_type_se3_trajectory<T>::value, bool>::type = true>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const T& trajectory, double u) {
    ...
}

template <typename V, typename enable_if<is_type_so2_trajectory<V>::value, bool>::type = true>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const V& trajectory, double u) {
    ...
}

Another easier way of writing this, if we ignore the type part:

template <typename T, typename enable_if<is_type_se3_trajectory<T>::value>::type* = nullptr>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const T& trajectory, double u) {
    ...
}

template <typename V, 
typename enable_if<is_type_so2_trajectory<V>::value>::type* = nullptr>
PathPointType GetNextGoal(
		const RobotInterface& robot_api, const V& trajectory, double u) {
    ...
}

Now let’s come back to the missing part: what is is_type_se3_trajectory? We have to define it ourselves:

template<typename T>
struct is_type_se3_trajectory {
  static const bool value = false; // Compile time Constant
};

template<typename T>
/**
 * SE3TrajectoryExt is an extended version of SE3Trajectory, which itself 
 * is templatized for flexible use of float/double/integer etc
 */
struct is_type_se3_trajectory<SE3TrajectoryExt<T>> {
  static const bool value = true;
};

template<typename T>
struct is_type_se3_trajectory<SpatialVelocityTrajectoryExt<T>> {
  static const bool value = true;
};

We can always add new types if we have more Trajectory -qualified types in the future. This is a good example of the Open-Closed Principle. This actually introduces another important concept in template programming, traits. We will stop here and talk about it in the next article.




Enjoy Reading This Article?

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

  • C++ Template Function Basics
  • Trajectory Basics I: Foundations
  • LLM Study Notes III: Post-Training