8
\$\begingroup\$

From a previous question I got an answer that included some template magic (that to be blunt was mind-boggling (as I could not understand it)).

So I have been trying to achieve the same results (because trying helps me learn).
To make sure I have learn correctly I am putting it here for comment. Hopefully it will also help somebody else (and you never know it may encourage me to write a blog post about it).

Template based ranges (I am sure it has been done to death).

The idea you provide a range that expanded by the template to make writing code easier. So the code I use to test it is working correctly.

template<typename>
struct printNumberRange;

// Only a specialization for my range is implemented.
template<int... N>
struct printNumberRange<Sizes<N...>>
{
    static void call()
    {
        std::vector<int> v {N...};
        std::copy(std::begin(v), std::end(v), std::ostream_iterator<int>(std::cout, "\n"));
    }
};

// Function to deduce the arguments and
// Call the print correctly.
template<int S, int E>
void printRange()
{
    print<typename Range<S,E>::type>::call();
}

int main()
{
    // Print the range using a template
    printRange<3,8>();
}

Version 1

The template code I started with:

template<int... N>
struct Sizes
{
    typedef Sizes<N...>       type;
};

template<int C, int P, int... N>
struct GetRange
{
    typedef typename GetRange<C-1, P+1, N..., P>::type      type;
};
template<int P, int... N>
struct GetRange<0, P, N...>
{
    typedef typename Sizes<N..., P>::type                   type;
};

template<int S, int E>
struct Range
{
    typedef typename GetRange<E-S, S>::type       type;
};

But it seems the trend nowadays is to use inheritance to get rid of the ugly typedef typename .... at each level:

Version 2

This should be exactly the same.
But we use inheritance to get the type of the terminal class in the recursion. Personally I find this much harder to read than the previous version. But it is more compact.

template<int... N>
struct Sizes
{
    typedef Sizes<N...>       type;
};

template<int C, int P, int... N>
struct GetRange: GetRange<C-1, P+1, N..., P>
{};
template<int P, int... N>
struct GetRange<0, P, N...>: Sizes<N...>
{};

template<int S, int E>
struct Range: GetRange<E-S+1, S>
{};

I can specialize printNumberRange to take a range directly.

template<int S, int E>
struct printNumberRange<Range<S, E>>:
     printNumberRange<typename Range<S,E>::type>  // Inherits from the version
{};                                               // That takes a Sizes<int...>

Then the print becomes:

int main()
{
   printNumberRange<Range<4,18>>::call();
}

Any comments on the Range stuff or the test harness welcome.

\$\endgroup\$
7
\$\begingroup\$

Generally speaking, it's good and works great (version 2 seems better). There are some things that could be changed/added though:

  • In C++, ranges tend to be [begin, end) ranges. Your compile-time integer ranges are actually [begin, end] ranges. The last element should not be included in the range. Therefore, you change this code:

    template<int S, int E>
    struct Range: GetRange<E-S+1, S>
    {};
    

    By this one:

    template<int S, int E>
    struct Range: GetRange<E-S, S>
    {};
    

    I tried it and it even works for Range<8,8> by producing an empty range.

  • Currently, your Range only works for increasing ranges of values (and empty ones). You could modify your code so that it also works with decreasing ranges of values. What I did is probably not really clean, but it works (take it as a proof of concept). I replaced Range and GetRange by the following classes:

    template<int C, int P, int... N>
    struct GetIncreasingRange:
        GetIncreasingRange<C-1, P+1, N..., P>
    {};
    
    template<int C, int P, int... N>
    struct GetDecreasingRange:
        GetDecreasingRange<C+1, P-1, N..., P>
    {};
    
    template<int P, int... N>
    struct GetIncreasingRange<0, P, N...>:
        Sizes<N...>
    {};
    
    template<int P, int... N>
    struct GetDecreasingRange<0, P, N...>:
        Sizes<N...>
    {};
    
    template<int S, int E, bool Increasing=(S<E)>
    struct Range;
    
    template<int S, int E>
    struct Range<S, E, true>:
        GetIncreasingRange<E-S, S>
    {};
    
    template<int S, int E>
    struct Range<S, E, false>:
        GetDecreasingRange<E-S, S>
    {};
    

    These ranges are [begin, end) ranges and also work for empty ranges.

  • Another possible improvement would be to let the user choose the integer type he wants to use for his range. That is actually what is done in the C++14 class std::integer_sequence, provided along with std::index_sequence which is its specialization for a range of std::size_t values. Here is a possible implementation.

  • Also, in your function call, you can drop the std::vector and replace it by std::array. The number of values is known at compile time (thanks to the operator sizeof...), so there's no need to use a dynamic storage container.

    static void call()
    {
        std::array<int, sizeof...(N)> v { N... };
        std::copy(std::begin(v), std::end(v), std::ostream_iterator<int>(std::cout, "\n"));
    }
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.