c++templatesdesign-patterns

Can template tag be inside template class itself?


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>;                                                                  

Solution

  • 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 ;).