Since C++17, using if constexpr
is very practical. Though, if you have many differences you may want to separate the floating point implementation from the integral type implementation completely.
All the below implementations require #include <type_traits>
and should also be followed by this free function:
template<class X>
Operand<X> operator%(const Operand<X>& l, const Operand<X>& r) {
Operand rv{l};
rv %= r;
return rv;
}
One way of doing it would be to use the CRTP:
template<class, bool> struct impl;
template<template<class> class O, class X>
struct impl<O<X>, true> { // specialization for floating point types
using op_type = O<X>;
// implement all special floating point behavior here:
op_type& operator%=(const op_type& other) {
op_type* This = static_cast<op_type*>(this);
This->val = std::fmod(This->val, other.val);
return *This;
}
};
template<template<class> class O, class X>
struct impl<O<X>, false> { // specialization for integral types
using op_type = O<X>;
// implement all special functions for integral types here:
op_type& operator%=(const op_type& other) {
auto This = static_cast<op_type*>(this);
This->val %= other.val;
return *This;
}
};
template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand : public impl<Operand<X>, std::is_floating_point_v<X>> {
private:
using impl_type = impl<Operand<X>, std::is_floating_point_v<X>>;
friend impl_type;
X val;
public:
Operand() = default;
Operand(X v) : val(v) {}
// implement functions common to integral and floating point types here
};
Without CRTP, it could look like this:
template<class, bool> struct impl;
template<class X>
struct impl<X, true> { // specialization for floating point types
X remainder(X l, X r) const {
return std::fmod(l, r);
}
};
template<class X>
struct impl<X, false> { // specialization for integral types
X remainder(X l, X r) const {
return l % r;
}
};
template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand : private impl<X, std::is_floating_point_v<X>> {
private:
X val;
public:
Operand& operator%=(const Operand& other) {
val = remainder(val, other.val);
return *this;
}
};
If you want to select the member function implementation using SFINAE:
template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand {
private:
X val{};
public:
Operand() = default;
Operand(X v) : val(v) {}
template<class Y = X, std::enable_if_t<std::is_same_v<X, Y>, int> = 0>
std::enable_if_t<std::is_floating_point_v<Y>, Operand&>
operator%=(const Operand& other) {
val = std::fmod(val, other.val);
return *this;
}
template<class Y = X, std::enable_if_t<std::is_same_v<X, Y>, int> = 0>
std::enable_if_t<std::is_integral_v<Y>, Operand&>
operator%=(const Operand& other) {
val %= other.val;
return *this;
}
};
Since C++17, using constexpr if is often preferred over the SFINAE method:
template <class X, std::enable_if_t<std::is_arithmetic_v<X>, int> = 0>
class Operand {
private:
X val{};
public:
Operand() = default;
Operand(X v) : val(v) {}
Operand& operator%=(const Operand& other) {
if constexpr (std::is_floating_point_v<X>) {
val = std::fmod(val, other.val);
} else {
val %= other.val;
}
return *this;
}
};