Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why for-loop isn't a compile time expression and extended constexpr allows for-loop in a constexpr function

I wrote code like this

#include <iostream>
using namespace std;
constexpr int getsum(int to){
    int s = 0;
    for(int i = 0; i < to; i++){
        s += i;
    }
    return s;
}
int main() {
    constexpr int s = getsum(10);
    cout << s << endl;
    return 0;
}

I understand that it works because of extended constexpr. However in this question why-isnt-a-for-loop-a-compile-time-expression, the author gave his code as follow:

#include <iostream>
#include <tuple>
#include <utility>

constexpr auto multiple_return_values()
{
    return std::make_tuple(3, 3.14, "pi");
}

template <typename T>
constexpr void foo(T t)
{
    for (auto i = 0u; i < std::tuple_size<T>::value; ++i)
    {
        std::get<i>(t);
    }    
}

int main()
{
    constexpr auto ret = multiple_return_values();
    foo(ret);
}

It could not compile by GCC5.1, however, after replacing std::get<i>(t); with something without a specified template parameter his code did work. After reading answers under this question I found their main point is constexpr for creates trouble for the compiler so for-loop is used at run-time. So it makes me confused, on one hand, for-loop is at run-time, on the other, the loop is in a constexpr function, so it must be calculated at compile time, so there seems to be a contradiction. I just wonder where did I make a mistake.

like image 718
calvin Avatar asked Feb 02 '17 14:02

calvin


3 Answers

Your​ function need to be valid both at runtime and at compile time. So your Variant i can be seen as a runtime variable. If the function is executed at compile time, the variable i is part of the compiler's runtime. So a int in a constexpr function is under the same rules as a int in a non-constexpr function.

What you can do is to create your​ own constexpr for loop:

template<typename F, std::size_t... S>
constexpr void static_for(F&& function, std::index_sequence<S...>) {
    int unpack[] = {0,
        void(function(std::integral_constant<std::size_t, S>{})), 0)...
    };

    (void) unpack;
}

template<std::size_t iterations, typename F>
constexpr void static_for(F&& function) {
    static_for(std::forward<F>(function), std::make_index_sequence<iterations>());
}

Then, you can use your static_for like this:

static_for<std::tuple_size<T>::value>([&](auto index) {
    std::get<index>(t);
});

Note that lambda function cannot be used in constexpr function until C++17, so you can instead roll your own functor:

template<typename T>
struct Iteration {
    T& tuple;

    constexpr Iteration(T& t) : tuple{t} {}

    template<typename I>
    constexpr void operator() (I index) const {
        std::get<index>(tuple);
    }
};

And now you can use static_for like that:

static_for<std::tuple_size<T>::value>(Iteration{t});
like image 187
Guillaume Racicot Avatar answered Nov 15 '22 04:11

Guillaume Racicot


This really has nothing to do with whether or not the constexpr function needs to be able to be runnable at compile-time (tuple_size<T>::value is a constant expression regardless of whether the T comes from a constexpr object or not).

std::get<> is a function template, that requires an integral constant expression to be called. In this loop:

for (auto i = 0u; i < std::tuple_size<T>::value; ++i)
{
    std::get<i>(t);
}    

i is not an integral constant expression. It's not even constant. i changes throughout the loop and assumes every value from 0 to tuple_size<T>::value. While it kind of looks like it, this isn't calling a function with different values of i - this is calling different functions every time. There is no support in the language at the moment for this sort of iteration, and this is substantively different from the original example, where we're just summing ints.

That is, in one case, we're looping over i and invoking f(i), and in other, we're looping over i and invoking f<i>(). The second one has more preconditions on it than the first one.


If such language support is ever added, probably in the form of a constexpr for statement, that support would be independent from constexpr functions anyway.

like image 41
Barry Avatar answered Nov 15 '22 03:11

Barry


Some better approach as Guillaume Racicot have mentioned with workaround of a bit unfinished constexpr support and std::size in such compilers like Visual Studio 2015 Update 3 and so.

#include <tuple>

#include <stdio.h>
#include <assert.h>

// std::size is supported from C++17
template <typename T, size_t N>
constexpr size_t static_size(const T (&)[N]) noexcept
{
    return N;
}

template <typename ...T>
constexpr size_t static_size(const std::tuple<T...> &)
{
    return std::tuple_size<std::tuple<T...> >::value;
}

template<typename Functor>
void runtime_for_lt(Functor && function, size_t from, size_t to)
{
    if (from < to) {
        function(from);
        runtime_for_lt(std::forward<Functor>(function), from + 1, to);
    }
}

template <template <typename T_> class Functor, typename T>
void runtime_foreach(T & container)
{
    runtime_for_lt(Functor<T>{ container }, 0, static_size(container));
}

template <typename Functor, typename T>
void runtime_foreach(T & container, Functor && functor)
{
    runtime_for_lt(functor, 0, static_size(container));
}

template <typename T>
void static_consume(std::initializer_list<T>) {}

template<typename Functor, std::size_t... S>
constexpr void static_foreach_seq(Functor && function, std::index_sequence<S...>) {
    return static_consume({ (function(std::integral_constant<std::size_t, S>{}), 0)... });
}

template<std::size_t Size, typename Functor>
constexpr void static_foreach(Functor && functor) {
    return static_foreach_seq(std::forward<Functor>(functor), std::make_index_sequence<Size>());
}

Usage:

using mytuple = std::tuple<char, int, long>;

template <typename T>
struct MyTupleIterator
{
    T & ref;

    MyTupleIterator(T & r) : ref(r) {}

    void operator() (size_t index) const
    {
        // still have to do with switch
        assert(index < static_size(ref));
        size_t value;
        switch(index) {
            case 0: value = std::get<0>(ref); break;
            case 1: value = std::get<1>(ref); break;
            case 2: value = std::get<2>(ref); break;
        }
        printf("%u: %u\n", unsigned(index), unsigned(value));
    }
};

template <typename T>
struct MyConstexprTupleIterator
{
    T & ref;

    constexpr MyConstexprTupleIterator(T & r) : ref(r) {}

    constexpr void operator() (size_t index) const
    {
        // lambda workaround for:
        //  * msvc2015u3: `error C3250: 'value': declaration is not allowed in 'constexpr' function body`
        //  * gcc 5.x: `error: uninitialized variable ‘value’ in ‘constexpr’ function`
        [&]() {
          // still have to do with switch
          assert(index < static_size(ref));
          size_t value;
          switch(index) {
              case 0: value = std::get<0>(ref); break;
              case 1: value = std::get<1>(ref); break;
              case 2: value = std::get<2>(ref); break;
          }
          printf("%u: %u\n", unsigned(index), unsigned(value));
        }();
    }
};

int main()
{
    mytuple t = std::make_tuple(10, 20, 30);
    runtime_foreach<MyTupleIterator>(t);

    mytuple t2 = std::make_tuple(40, 50, 60);
    runtime_foreach(t2, [&](size_t index) {
        // still have to do with switch
        assert(index < static_size(t2));
        size_t value;
        switch(index) {
            case 0: value = std::get<0>(t2); break;
            case 1: value = std::get<1>(t2); break;
            case 2: value = std::get<2>(t2); break;
        }
        printf("%u: %u\n", unsigned(index), unsigned(value));
    });

    mytuple t3 = std::make_tuple(70, 80, 90);
    static_foreach<std::tuple_size<decltype(t3)>::value>(MyConstexprTupleIterator<mytuple>{t3});

    return 0;
}

Output:

0: 10
1: 20
2: 30
0: 40
1: 50
2: 60
0: 70
1: 80
2: 90
like image 26
Andry Avatar answered Nov 15 '22 02:11

Andry