I recently had a need to use template meta-programming to enable a particular set of constructors on a templated type only if one of the template parameters matched a particular value. Template specialization would have required copying a very large amount of code around, or using some undesirable inheritance tricks to get things to work.
The normal means of using std::enable_if<>
for functions and methods would not work because C++ constructors do not have return values. I found a work-around, but it has a few caveats that are worth pointing out for anyone else in a similar situation.
Background
The code in question is a simple linear algebra library, allowing users to build vectors of various component types and sizes. It’s like a very pared down GLM (there’s unfortunately a reason that I need to reimplement code that already exists, is tested, documented, and works).
The basic vector type looks something like this:
template <typename Type, size_t Size>
struct Vector
{
// all components uninitialized
Vector() {} // "= default" in C++11
// all components set to a single value
Vector(Type value)
{
for (size_t i = 0; i != Size; ++i)
a[i] = value;
}
// components array
Type a[Size];
};
The Problem
What I wanted to add was a means of initializing the components of the vectors for the common vector sizes I work with: 2, 3, and 4. This requires adding new constructors:
Vector(Type x, Type y)
{
a[0] = x; a[1] = y;
}
Vector(Type x, Type y, Type z)
{
a[0] = x; a[1] = y; a[2] = z;
}
Vector(Type x, Type y, Type z, Type w)
{
a[0] = x; a[1] = y; a[2] = z; a[3] = w;
}
However, that posed a bit of a problem. It meant that I could construct a 2-component vector with 4 values (and get a crash, or require extra checks), or construct a 4-component vector with 2 values (leaving the other two uninitialized). Both are easily worked around in a way that results in an error-prone API, but I wanted a work-around that gave me the ideal API: vectors can be initialized only by passing in the number of components that the vector has, with compile-time errors.
Again, because these types have a lot of other code (much more than the simple example), specializing the templates wasn’t an option. Using the static_assert feature of C++11 does not work, as it would always fail. Opting to use initializer lists was off the table as Visual C++ does not yet support those.
What I needed was good old enable_if. However, it requires the function to be a templated function. These constructors, while part of a templated class, were not themselves templated. As there is no return value, I could not use enable_if the standard way. The trick was to use enable_if on the constructor parameters. However, there are some special considerations to get this to work properly.
// this does not work because constructors have no return type
template <typename FirstType>
typename std::enable_if<Size == 2, void>::type Vector(FirstType x, Type y) { ... }
// this does not work because type-deduction for FirstType will not work
template <typename FirstType>
Vector(typename std::enable_if<Size == 2, FirstType>::type x, Type y) { ... }
// this will work
template <typename SecondType>
Vector(typename std::enable_if<Size == 2, Type>::type x, SecondType y) { ... }
The template FirstType
must not be used as the enable_if
type. Doing that will remove the ability of the compile to do proper type deduction. The compiler would be required to know what FirstType
is to evaluate enable_if
, but it needs to evaluate enable_if
to determine what FirstType
is.
The Solution
Moving the template type to the second parameter, and using the Vector’s Type for the first parameter’s enable_if
type. Now the template parameter can be deduced. However, it’s necessary for enable_if
’s condition to itself be dependent on the template type, otherwise the SFINAE rules never kick in. The trick I found was to just insert some kind of type check into the size comparison, for example to test that InType
is convertible to the vector Type
.
// initialize 2-component vectors directly
template <typename InType>
Vector(typename std::enable_if<std::is_convertible<InType, Type>::value && Size == 2, Type>::type x, InType y)
{
a[0] = x; a[1] = Type(y);
}
// initialize 3-component vectors directly
template <typename InType>
Vector(typename std::enable_if<std::is_convertible<InType, Type>::value && Size == 3, Type>::type x, InType y, Type z)
{
a[0] = x; a[1] = Type(y);
}
// initialize 4-component vectors directly
template <typename InType>
Vector(typename std::enable_if<std::is_convertible<InType, Type>::value && Size == 4, Type>::type x, InType y, Type z, Type w)
{
a[0] = x; a[1] = Type(y); a[2] = z; a[3] = w;
}
Also note that only one of the parameters is of type InType. We needed one parameter to be a templated type to use enable_if, but there’s no reason for all of them to be templated. In fact, by making all the parameters (after the first) of type InType, it might cause extra conversions in some (most likely very rare) cases.
Bonus Hack: Friendly Member Accessors
Another feature I wanted for the Vector type was the ability to access members using .x, .y, .z, and .w notation (if the vector was of the appropriate size). The usual way to implement this while retaining the generic subscript access ability is to use an anonymous union wrapping the array and an anonymous struct.
union
{
struct { Type x, Type y, Type z, Type w; };
Type a[Size];
};
Obviously the above code is correct for a Vector of size 4, but not for any smaller size. (Whether it’s correct or not for larger sizes is arguable; I chose “no”.)
The solution here was to replace the anonymous union and struct with template specialized struct to use a base class for Vector that offered the accessors I wanted. I added these:
template <typename Type, size_t Size> struct Components
{
Type a[Size];
};
template <typename Type> struct Components<Type, 2>
{
union
{
struct
{
Type x, y;
};
Type a[2];
};
};
template <typename Type> struct Components<Type, 3>
{
union
{
struct
{
Type x, y, z;
};
Type a[3];
};
};
template <typename Type> struct Components<Type, 4>
{
union
{
struct
{
Type x, y, z, w;
};
Type a[4];
};
};
The Vector type could then inherit from this to get its members, rather than defining its own:
template <typename Type, size_t Size>
Vector : public Components<Type, Size>
{
// ...
};
C++11 and Why Visual Studio Makes Me Drink
Naturally, at this point it almost seems worth-while to just move all of that ugly template meta-programming with enable_if
into constructors in the specialized Components type. That would simplify everything. Well, it would if and only if I could make use of the C++11 capability to inherit constructors. Remember that in C++98, constructors are not inherited automatically, and it’s necessary to manually wrap parent constructors to use them. C++11 allows a simple syntax to work around this, and hence remove all the enable_if
mess from above:
// specialized 2-component version
template <typename Type> struct Components<Type, 2>
{
union
{
struct
{
Type x, y;
};
Type a[2];
};
// retain trivial default constructor
Components() = default;
// the desired convenience constructor
Components(Type x, Type y) : x(x), y(y)
};
template <typename Type, size_t Size>
Vector : public Components<Type, Size>
{
// pull in the convenience constructor from the specialized Components type
using Components<Type, Size>::Components;
// ...
};
Naturally (and quite unfortunately), Visual Studio does not support this feature yet, so it’s a complete non-option for projects that need to compile on Microsoft’s toolchain. For now, the enable_if construct tricks remain.