My take at times

This article is inspired by Dmitry Ledentsov’s blog post: C++ version of ruby’s Integer::times via user-defined literals.

In this post Dmitry (aka DLed) is inspired to replicate Ruby’s syntax for repeating actions. In Ruby we can write:

    42.times do ...

Where … is some bit of code to be repeated, in this case, forty-two times. Dmitry’s blog shows off the Modern C++ feature of user-defined literals.

But I’m a bit old school on this and I don’t care for that syntax. But the post did inspire me to play around with how I’d implement this with a more traditional syntax. Along the way we’ll touch on decltype, template function overloading, SFINAE, variadic template parameters, assert(), type functions, perfect forwarding, and, of course, trying to write our code as generally as possible.

First cut

So the first cut is pretty straight forward. We want to write this:

    int main()
    {
        times(3, []{std::cout << "bla/n";});
    }

So we need this:

    #include <iostream>

    template <class Callable>
    void times(int n, Callable f)
    {
        for (int i{0}; i != n; ++i)
        {
            f();
        }
    }

We could avoid the template by writing the function like this:

    void times(int n, void(*f)())
    {
        for (int i{0}; i != n; ++i)
        {
            f();
        }
    }

Or, more clearly:

    using Callable = void(*)();
    void times(int n, Callable f)
    {
        for (int i{0}; i != n; ++i)
        {
            f();
        }
    }

But this would only work for functions and function pointers that are a perfect match for this signature. A function that returns a non-void return type, couldn’t be passed, even though it would work for our purposes.

But more importantly, this approach would only work for functions and function pointers. It would not work for function objects and, most importantly of all, it wouldn’t work or lambdas.

Of course we want to be as general as possible, so we’ll do this:

    #include <iostream>

    template<class Count, class Callable>
    void times(Count n, Callable f)
    {
        for (Count i{0}; i != n; ++i)
            {
            f();
            }
    }

But just as Dmitry does, we want to be able to also support a version where the Callable takes a parameter that is the loop iteration. This is one thing that I miss when using the Modern range based for syntax–sometimes I want to know what iteration we are on.

Iteration count

So we want to be able to write this:

    int main()
    {
        times(3,
              [](int i){std::cout << "counting: " << i << "/n";});

        times(3, []{std::cout << "bla/n";});
    }

We use the trick suggested by Kirk Shoop (and improved by Alberto Ganesh Barbati) to implement this:

    #include <iostream>

    // #1 without argument
    template<class Count,
             class Callable,
             class Enable = decltype(std::declval<Callable>()()())
            >
    void times(Count n, Callable f, ...)
    {
        for (Count i{0}; i != n; ++i)
            {
            f();
            }
    }

    // #2 with argument
    template<class Count,
             class Callable,
             class Enable =
                   decltype(std::declval<Callable>()(Count{0}))
            >
    void times(Count n, Callable f)
    {
        for (Count i{0}; i != n; ++i)
            {
            f(i);
            }
    }

Notice that the variadic parameter pack () is introduced to prevent the compiler from objecting to the fact that we are essentially defining the same template twice with the only difference being the default type of the Enable parameter.

By introducing the parameter pack argument we make the two definitions of times different so that the compiler will accept them both as overloads. (Two definitions that only differ in a default value are a redefinition, not an overload.) But because a parameter pack can be empty (and will be in this use case), the two definitions are really the same in practice. It doesn’t matter which definition gets the parameter pack because, as we just discussed, it will be empty.

The reason that the compiler rejects a redefinition is because it won’t know which function to call, the original or the redefinition. By using the empty parameter pack trick to fool the compiler into thinking that these are two different definitions when in practice they really aren’t, aren’t we just setting ourselves up for failure when we try to call the function? How will the compiler know which definition to use when we make a class for which the parameter pack is empty?

The trick here is that we are using something called SFINAE. If you don’t know about enable_if and/or SFINAE, then you can either just chalk this up to template magic that you’ll learn later, or you could watch this talk and learn it now.

In this code we aren’t using enable_if, which is the typical go to tool for SFINAE, instead we have the Enable parameter which is functioning like enable_if. That is to say that if the Callable doesn’t take a Count argument (initialized by 0) then the definition that I’ve labeled #2 with argument gets SFINAE’ed out and if the Callable can’t be called without arguments, then the definition that I’ve labeled #1 without argument gets SFINAE’ed out.

Since the Enable parameter is only used for SFINAE, we never pass it a value and only use the default. The default value expression uses std::declval, which was created specifically for this type of situation. If we didn’t have it, we’d end up using something like this:

class Enable = decltype((*(Callable*) nullptr)())

declval just creates a reference type of its template arguments to it can be used in expressions in unevaluated contexts like decltype().

Exploiting the generality

So far we haven’t done anything that Dmitry didn’t accomplish and some people might prefer his (Ruby inspired) syntax. But I prefer the traditional syntax because I think it is more general. Here is why I think so:

Now that we are taking an iterator count, we might want to play games with that. We’d like to index between any two arbitrary values. We could index between 10 to 15. We could even handle indexing in reverse order:

    int main()
    {
        times(10,
              16,
              [](int i){std::cout << "index: " << i << "/n";});
        times(15,
              9,
              [](int i){std::cout << "index: " << i << "/n";});

        times(3,
              [](int i){std::cout << "counting: " << i << "/n";});

        times(3, []{std::cout << "bla/n";});
    }

The implementations is the same as before with the addition of these definitions:

    // #1 without argument
    template<
            class Count,
            class Callable,
            class Enable = decltype(std::declval<Callable>()())
            >
    void times(Count start, Count end, Callable f, ...)
    {
        if (start < end)
            {
            for (auto i(start); i != end; ++i)
                {
                f();
                }
            }
        else if (start > end)
            {
            for (auto i(start); i != end; --i)
                {
                f();
                }
            }
    }

    // #2 with argument
    template<
            class Count,
            class Callable,
            class Enable =
                  decltype(std::declval<Callable>()(Count{0}))
            >
    void times(Count start, Count end, Callable f)
    {
        if (start < end)
            {
            for (auto i(start); i != end; ++i)
                {
                f(i);
                }
            }
        else if (start > end)
            {
            for (auto i(start); i != end; --i)
                {
                f(i);
                }
            }
    }

Notice that if the Callable doesn’t take the index argument, we don’t really care what order we index, but we still need to get the direction correct in order to know if we need to increment or decrement.

Next step

Now that we’ve done this, I’d guess that you see the next step coming. We’d like to increment (or decrement) by values other than one. So we’d like to be able to do this:

    int main()
    {
        times(10,
              20,
              3,
              [](int i){std::cout << "i: " << i << "/n";});
        times(20,
              10,
              3,
              [](int i){std::cout << "i: " << i << "/n";});

        times(10,
              16,
              [](int i){std::cout << "index: " << i << "/n";});
        times(15,
              9,
              [](int i){std::cout << "index: " << i << "/n";});

        times(3,
              [](int i){std::cout << "counting: " << i << "/n";});

        times(3, []{std::cout << "bla/n";});
    }

The implementations is the same as before with the addition of these definitions:

    // #1 without argument
    template<
            class Count,
            class Callable,
            class Enable =
                  decltype(std::declval<Callable>()())
            >
    void times(Count start,
               Count end,
               Count delta,
               Callable f,
               ...)
    {
        assert(delta != 0);
        if (delta < 0)
            {
            delta = 0 - delta;
            }
        if (start < end)
            {
            for (auto i(start); i < end; i += delta)
                {
                f();
                }
            }
        else if (start > end)
            {
            for (auto i(start); i > end; i -= delta)
                {
                f();
                }
            }
    }

    // #2 with argument
    template<
            class Count,
            class Callable,
            class Enable =
                  decltype(std::declval<Callable>()(Count{0}))
            >
    void times(Count start, Count end, Count delta, Callable f)
    {
        assert(delta != 0);
        if (delta < 0)
            {
            delta = 0 - delta;
            }
        if (start < end)
            {
            for (auto i(start); i < end; i += delta)
                {
                f(i);
                }
            }
        else if (start > end)
            {
            for (auto i(start); i > end; i -= delta)
                {
                f(i);
                }
            }
    }

If the delta parameter is zero, this is a logic error, so we assert(), as is appropriate for logic errors. That means that we also need:

    #include <cassert>

The direction of the delta is ambiguous in normal conversation. One could say “Count from twenty to ten by threes.” and not be expected to say “Count from twenty to ten by negative threes.” So we’ll take it as given that the direction of the delta is determined by the start and end values of the indexing range.

Note that we need to change our terminating condition. Up until now we’ve followed the STL inspired practice of terminating with the general:

i != end

but now that we have non-monatomic incrementing, we need the less general loop termination to avoid overshooting the end point.

Arbitrary parameters

The last generality I want to address is to allow us to pass arbitrary values to be used as parameters to the Callable. We see this pattern in the call to std::async(). We’d like to be able to write:

    int main()
    {
        times(3,
              6,
              1,
              [](int i) {std::cout << "4: " << i << "\n";},
              4);
        times(3,
              6,
              1,
              [](int i, float f)
                  {std::cout << i << " " << f << "\n";},
              4,
              0.4);
        times(3, 6, 1, [] {std::cout << "no params\n";});
        times(3,
              6,
              1,
              [](int i) {std::cout << "count: " << i << "\n";});
    }

That is to say, we want to be able to pass any number of any type of parameters after the Callable and these parameters should be perfect forwarded to the Callable.

We don’t want to break any of our existing functionality (which depends on using the empty variadic parameter pack trick) and we don’t want to cause any ambiguity if we happen to pass a parameter of the same type as the Count parameter.

For our implementation we just need to add a new definition that will only be enabled (using our friend std::enable_if) when there are parameters passed after the Callable. That is, when the parameter pack is not empty.

Here is our implementation. Note that we need to add this same definition for the flavors that have one and two Count parameters before the Callable.

    template<
            class Count,
            class Callable,
            class... Args
            >
    auto times(Count start,
               Count end,
               Count delta,
               Callable f,
               Args&&... args) ->
    std::enable_if_t<(sizeof...(args) > 0)>
    {
        assert(delta != 0);
        if (delta < 0)
            {
            delta = 0 - delta;
            }
        if (start < end)
            {
            for (auto i(start); i < end; i += delta)
                {
                f(std::forward<Args>(args)...);
                }
            }
        else if (start > end)
            {
            for (auto i(start); i > end; i -= delta)
                {
                f(std::forward<Args>(args)...);
                }
            }
    }

In the previous versions, we didn’t need to name the parameter pack type or value because we didn’t ever use it. (It was only used to prevent the compiler from complaining about our redefinition of times().) But here we are going to use the parameter pack (we are going to perfect forward it to the Callable), so we need to give it a type (our template parameter Args&&) and a name, args.

The && following the template type means that this is a forwarding reference. It has the same syntax as an rvalue reference, but due to the magic of reference collapsing, args is a pack of lvalue and rvalue references depending on what was deduced from what is passed. When we pass them on with std::forward<Args>(args)… the compiler will pass them to the Callable so that each parameter is the type (rvalue or lvalue) that the reference is bound to.

This is the secret of perfect forwarding. How this happens is really beyond the scope of what I want to talk about here, but I did want to show how to perfect forward an arbitrary set of parameters à la async(), emplace(), and make_shared().

We are also using std::enable_if to disable this definition when the parameter pack is empty.

Template type functions, which is what enable_if is, are templates that take types (and compile time constant values) as parameters and return a type (as a nested type named type). enable_if takes two parameters, one a compile time Boolean constant and the other a type. If the constant is true, then the type function returns the passed type. If the constant is false, the function definition is disabled, that is it is SFINAE’ed out of the overload set. The overload set is the set of functions that the compiler will consider when, in this case, we call times.

Since enable_if is a type function, we would need to write it like this:

    typename std::enable_if<(sizeof...(args) > 0), void>::type

except there are some short cuts. The second parameter (the type) defaults to void which is what we want in this case, so we don’t need to specify it. Also we can use enable_if_t. This is a type alias to the type defined by the nested type referred to as ::type, so we end up just writing:

    std::enable_if_t<(sizeof...(args) > 0)>

Note that we need the parentheses around the compile-time constant Boolean expression because it contains the “>” character which will be seen as closing the template parameter block if it is not in parentheses.

The magic of enable_if is that it makes a function template definition “go away” (when the Boolean value is false) no matter where it is in the definition. It can be any of the parameter types, a default value of a parameter type, or the return value. Since I’m using it as the return value here and because it depends on an identifier defined in the parameter block (args), we need to use the trailing return type syntax:

    auto times(...) -> return_type

The Boolean compile time constant that we are passing to enable_if is sizeof…(args) > 0. This use of sizeof takes a parameter pack (that’s what the is for) and, instead of returning a number of bytes as sizeof usually returns, it returns the number of parameters in the parameter back. In our case we don’t care about the number, we only care that it is non-zero, that is to say, that the parameter pack is not empty. If the parameter pack is empty for a particular call, the compiler ignores this definition for that particular call, because of enable_if. If the parameter pack is not empty (there are parameters after the Callable) then the definition is used and its return value is void.

Comments

This exercise was, to me, less about a useful implementation (unlike Dmitry, I’ve not used Ruby or times and don’t crave them) than about playing with implementing a simple library that explores some nice implementation techniques.

If you’d like to share your thoughts, please comment on the reddit post.


Updates

This post has been updated based on the reddit comments. My thanks to the all the commenters.

mark_99 pointed out that the ellipsis trick is classic varargs, not the modern variadic template parameter feature.

Bobby Graese? (TrueJournals) pointed out that I was needlessly using decltype to declare i.

Thanks to Alberto Ganesh Barbati (iaanus) for teaching me about declval.

The comments also contain an application of if constexpr by Vittorio Romeo  (SuperV1234) that really cleans up this exercise. I didn’t update for this because if constexpr is a C++17 feature.

Vittorio also suggests that “You should also consider passing f by forwarding-reference, in order to avoid unnecessary copies for non-zero-sized callable objects.” This is not consistent with the standard practice in the STL of just passing callables by copy. That is based on the assumption that callables are usually cheap to copy. There is a readability trade-off. You be the judge. Here is this suggestion applied to the first cut:

    #include <iostream>

    template <class Callable>
    void times(int n, Callable&& f)
    {
        for (int i{0}; i != n; ++i)
        {
            std::forward<Callable>(f)();
        }
    }

And thanks to Louis Dione for reminding us that Hana does it better.