While I was a teacher assistant and during my time helping new employees move from Python to C++, I have gotten one question often. Why can I not do a for loop like this for elm in range(0, 10) and you know what that is actually a fair question. In particular for arithmetic types… but what if you could part of the way?

Well first, let us look at a concept to define a type that must be an arithmetic type. This can be done fairly easy like this template<typename NumericType> concept Numeric = std::is_arithmetic<NumericType>::value;. Basically what this does is to create a type that must be numeric. I will show a little later what happens if you use one that is not.

Next let us generate the range, here I will use std::vector as my range container. This can be done very simply with the function below. This assume that start is smaller than end, otherwise it will not work.

template <Numeric T> constexpr std::vector<T> range(T start, T end)
{
    T size = end - start;
    std::vector<T> data(size);
    for (T i = 0; i < size; ++i)
    {
        data.at(i) = start + i;
    }
    return data;
}

Now let us test it. For this we will make a print_range function which takes start and end and calls range and prints the resulting “range”. Again we use the Numeric template type constrained to be a arithmetic type.

template <Numeric T> void print_range(T start, T end)
{
    std::cout << "[";
    for (const auto val : range(start, end))
    {
        std::cout << " " << std::to_string(val);
    }
    std::cout << " ]\n";
}

Finally our main function, where we test with size_t, int, and float.

int main(void)
{
    print_range(static_cast<size_t>(1), static_cast<size_t>(10));
    print_range(1, 10);
    print_range(1.0, 10.0);
}

The expected result is:

[ 1 2 3 4 5 6 7 8 9 ]
[ 1 2 3 4 5 6 7 8 9 ]
[ 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 ]

The program should be compiled with clang++ -std=c++20 or g++ -std=c++20. The full program is listed below:

#include <vector>
#include <concepts>
#include <iostream>
#include <string>

template<typename NumericType> concept Numeric = std::is_arithmetic<NumericType>::value;

template <Numeric T> constexpr std::vector<T> range(T start, T end)
{
    T size = end - start;
    std::vector<T> data(size);
    for (T i = 0; i < size; ++i)
    {
        data.at(i) = start + i;
    }
    return data;
}

template <Numeric T> void print_range(T start, T end)
{
    std::cout << "[";
    for (const auto val : range(start, end))
    {
        std::cout << " " << std::to_string(val);
    }
    std::cout << " ]\n";
}

int main(void)
{
    print_range(static_cast<size_t>(1), static_cast<size_t>(10));
    print_range(1, 10);
    print_range(1.0, 10.0);
}

Now let us say we had called print_range with "a" as start and "z" as end. Well then you would get the following compilation error:

main.cpp:34:5: error: no matching function for call to 'print_range'
   34 |     print_range("a", "z");
      |     ^~~~~~~~~~~
main.cpp:19:27: note: candidate template ignored: constraints not satisfied [with T = const char *]
   19 | template <Numeric T> void print_range(T start, T end)
      |                           ^
main.cpp:19:11: note: because 'const char *' does not satisfy 'Numeric'
   19 | template <Numeric T> void print_range(T start, T end)
      |           ^
main.cpp:6:50: note: because 'std::is_arithmetic<const char *>::value' evaluated to false
    6 | template<typename NumericType> concept Numeric = std::is_arithmetic<NumericType>::value;
      |                                                  ^
1 error generated.

Some final remarks. First,this can be made MUCH prettier (which I may show in a later post). Secondly, this is not very optimised and only serves as inspiration. Finally, play around with it.

./Lars