I have a templated class Bounded which have parameters:
I can do it if the struct defining policy is defined outside the Bounded class itself. Is it possible to define such struct as policy inside the Bounded class to limit scope or is there other solution to achieve the same?
I have tried it:
#include <cmath>
#include <numbers>
template <typename T, T Min, T Max, template<typename, T, T> class OverflowPolicy>
class Bounded : public OverflowPolicy<T, Min, Max>
{
T m_value;
public:
struct Limit
{
constexpr auto rangeReduction (T value) -> T
{
T result = std::fmod(value - Min, Max - Min) + Min;
if (result < Min)
result += Min;
return result;
}
};
struct Wrap
{
constexpr auto rangeReduction (T value)-> T
{
return (value < Min) ? Min : (value > Max) ? Max : value;
}
};
constexpr Bounded (T value) : m_value (OverflowPolicy<T, Min, Max>::rangeReduction(value)){}
constexpr explicit operator T () const {return m_value;}
};
constexpr auto tau = 2 * std::numbers::pi_v<float>;
using Radians = Bounded<float, 0.f, tau, Bounded::Wrap<float, 0.f, tau>;
auto main () -> int
{
return static_cast<float>(Radians(10);
}
This gives an excpected error as the 4th template argument is Bounded class itself.
<source>:31:55: error: template argument 4 is invalid
31 | using Radians = Bounded<float, 0.f, tau, Bounded::Wrap>;
You cannot inherit from an inner class. This
struct bar;
struct foo : bar { struct bar{}; };
is a simpler example to demonstrate the same. You can only inherit from a complete class. bar
is only complete when foo
is defined.
Next, you confuse the (class) template with a class in several instances. For example Bounded::Wrap<float, 0.f, tau>
makes no sense. Bounded
is a template, it needs the template arguments before you can name its inner type. Wrap
is not a template, it has no template arguments. To use it as policy for Bounded
it has to be a template.
Anyhow there is no way to make this work directly as desired. Because even ignoring the fact that you cannot inherit from an incomplete type, the definition of Radians
would still be recursive. You need a level of indirection, or just give up the idea to define Wrap
inside Bounded
.
You can define it inside some detail
namespace and then use it as the default policy:
#include <cmath>
#include <cmath>
#include <numbers>
namespace detail {
template <typename T, T Min, T Max>
struct Wrap
{
constexpr auto rangeReduction (T value)-> T
{
T result = std::fmod(value - Min, Max - Min) + Min;
if (result < Min)
result += Min;
return result;
}
};
template <typename T, T Min, T Max>
struct Limit
{
constexpr auto rangeReduction (T value) -> T
{
return (value < Min) ? Min : (value > Max) ? Max : value;
}
};
}
template <typename T, T Min, T Max, template<typename, T, T> class OverflowPolicy = detail::Wrap>
class Bounded : public OverflowPolicy<T, Min, Max>
{
T m_value;
public:
constexpr Bounded (T value) : m_value (OverflowPolicy<T, Min, Max>::rangeReduction(value)){}
constexpr explicit operator T () const {return m_value;}
};
constexpr auto tau = 2 * std::numbers::pi_v<float>;
using Radians = Bounded<float, 0.f, tau>; // use the default policy
auto main () -> int
{
return static_cast<float>(Radians(10)); // ) was missing
}
You also had the implementations of Wrap
and Limit
swapped.
If you merely want to limit or wrap a value, the code can be much simpler:
#include <cmath>
#include <cmath>
#include <numbers>
enum Overflow_behavior { wrap, limit};
template <typename T, T Min, T Max, Overflow_behavior overflow_behavior = wrap>
class Bounded
{
T m_value;
auto rangeReduction(T value) {
if constexpr (overflow_behavior == wrap) {
T result = std::fmod(value - Min, Max - Min) + Min;
if (result < Min)
result += Min;
return result;
} else {
return (value < Min) ? Min : (value > Max) ? Max : value;
}
}
public:
constexpr Bounded (T value) : m_value (rangeReduction(value)){}
constexpr explicit operator T () const {return m_value;}
};
constexpr auto tau = 2 * std::numbers::pi_v<float>;
using Radians = Bounded<float, 0.f, tau>;
int main ()
{
return static_cast<int>(Radians(10));
}
Next I'd realize that the class Bounded
merely modifies a T
value but does nothing else. Requiring to call a constructor and a conversion operator is too contrived when a function can do the same (note the need for a static cast above):
#include <cmath>
#include <cmath>
#include <numbers>
enum Overflow_behavior { wrap, limit};
template <typename T, T Min, T Max, Overflow_behavior overflow_behavior = wrap>
auto bound(T value) {
if constexpr (overflow_behavior == wrap) {
T result = std::fmod(value - Min, Max - Min) + Min;
if (result < Min)
result += Min;
return result;
} else {
return (value < Min) ? Min : (value > Max) ? Max : value;
}
}
constexpr auto tau = 2 * std::numbers::pi_v<float>;
int main ()
{
return bound<float,0.f,tau>(10);
}
Note how now all is nicely encapsulated in one function (if you want you could even get rid of the enum by using a plain bool
). I could go on. For limiting a value to a range there is already std::clamp
and I suppose also wrapping around can still be simplified further.
Last but not least, your code gives the false impression that Radians
would be a tagged type, but it isnt really. Note that
using Apples = Bounded<float, 0.f, tau>;
refers to the exact same type as Radians
. If you want Apples
and Radians
to be distinct types (which is probably a good idea) you need to do more (which I consider as beyond the scope of the question ;).