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.
1 Answer 1
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 replacedRange
andGetRange
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 withstd::index_sequence
which is its specialization for a range ofstd::size_t
values. Here is a possible implementation.Also, in your function
call
, you can drop thestd::vector
and replace it bystd::array
. The number of values is known at compile time (thanks to the operatorsizeof...
), 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")); }
Explore related questions
See similar questions with these tags.