Say I have written a generic function called interpolate
. Its signature is like so:
template<typename T>
T interpolate(T a, T b, float c);
Where a and b are the values to interpolate between, and c is a float in [0.0,1.0].
Iff T has T operator*(float)
and T operator+(T)
defined, I would like this to behave a certain way (linear interpolation). Otherwise, it would behave differently - in such a way that any T
is useable(nearest neighbor interpolation).
How can I achieve this behavior?
For example:
interpolate<std::string>("hello","world!", 0.798); //uses nearest neighbor, as std::string does not have the necessary operators
interpolate<double>(42.0,128.0, 0.5); //uses linear, as double has the needed operators
Note: this question is not on the implementation of these interpolation methods, just how to use templates to switch behavior of a function.
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
}
It's possible to provide an order of preference for overloaded functions. If the number of overloads is small, you can just use:
using prefer_overload_t = int;
using backup_overload_t = long;
template <typename T>
auto interpolate_impl(T a, T b, float c, prefer_overload_t)
-> std::enable_if_t<
std::is_same_v<T, decltype(a * c)>
&& std::is_same_v<T, decltype(a + b)>,
T
>
{
// linear interpolation
}
template <typename T>
T interpolate_impl(T a, T b, float c, backup_overload_t)
{
// nearest neighbor
}
template<typename T>
T interpolate(T a, T b, float c)
{
return interpolate_impl(std::move(a), std::move(b), c, prefer_overload_t());
}
Since it requires no conversion to go from int
to int
, the former overload is preferred, but is SFINAE'd out when it doesn't work.
If you want to order an arbitrary number of overloads, you'd have to use some special type like this:
template <std::size_t N>
struct rank : rank<N - 1>
{};
template <>
struct rank<0>
{};
Then, rank<N>
would be preferred over rank<N - 1>
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With