This sounds like a prime use case for tag dispatching:
We create two different tag classes to distinguish between the two use cases
struct linear_tag {};
struct nn_tag {};
template <typename T>
T impl(T a, T b, float c, linear_tag) {
// linear interpolation here
}
template <typename T>
T impl(T a, T b, float c, nn_tag) {
// nearest neighbor interpolation here
}
Now, we need to find out the tag type from T
:
template <typename T>
linear_tag tag_for(
T* p,
std::enable_if_t<std::is_same_v<T, decltype((*p + *p) * 0.5)>>* = nullptr
);
nn_tag tag_for(...); // Fallback
The first overload only exists if, for any T t
, the expression (t + t) * 0.5f
returns another T
.1 The second overload always exists, but because of the C-style variadic argument, it is never used unless the first overload doesn't match.
Then, we can dispatch to either version by creating the appropriate tag:
template <typename T>
T interpolate(T a, T b, float c) {
return impl(a, b, c, decltype(tag_for(static_cast<T*>(nullptr))){});
}
Here, decltype(tag_for(static_cast<T*>(nullptr)))
gives us the right tag type (as the return type of the correct overload of tag_for
).
You can add additional tag types with very little overhead, and test for arbitrarily complex conditions in the enable_if_t
. This particular version is C++17 only (because of is_same_v
), but you can just as easily make it C++11-compatible by using typename std::enable_if<...>::type
and std::is_same<...>::value
instead - it's just a bit more verbose.
1 This is what you specified in the question - but it is dangerous! If you use integers, for example, you will use nearest-neighbor interpolation because *
returns float
, not int
. You should instead test if the expression (*t + *t) * 0.5f
returns something that is convertible back to T
using a test such as std::is_constructible_v<T, decltype((*t + *t) * 0.5f)>
As a bonus, here is a c++20 concepts-based implementation that doesn't need tags anymore (as briefly mentioned in the comments). Unfortunately, there is no compiler that supports requires
on this level yet, and of course the draft standard is always subject to change:
template <typename T>
concept LinearInterpolatable = requires(T a, T b, float c) {
{ a + b } -> T;
{ a * c } -> T;
};
template <LinearInterpolatable T>
T interpolate(T a, T b, float c)
{
// Linear interpolation
}
template <typename T>
T interpolate(T a, T b, float c)
{
// Nearest-neighbor interpolation
}