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

3

8

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.

Gaudery answered 2/2, 2017 at 14:48 Comment(2)
constexpr doesn't mean it definitely runs at compile-time. constexpr functions are meant to work at both compile-time and runtime.Tacit
https://mcmap.net/q/683239/-possible-to-instantiate-templates-using-a-for-loop-in-a-c-14-constexpr-function I think this is the best answerSchnitzler
B
8

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.

Blather answered 2/2, 2017 at 15:4 Comment(0)
E
10

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});
Eleanor answered 2/2, 2017 at 15:22 Comment(5)
Why is so complicated? Is that not enough: template<typename F> constexpr void static_for_lt(F && function, size_t from, size_t to) { if (from < to) { static_for_lt(std::forward<F>(function), from + 1, to); } } ?Cohligan
@Cohligan I have difficulty understanding your code snippet. First you don't ever call function. Also, your code don't make from nor to available as compile time constants to the body of function.Eleanor
Could not edit my comment. I meant with the call inside the if. Why this does not solve the problem?Cohligan
I've found better workaround for not allowed declarations in constexpr functions: template <typename T> void static_consume(std::initializer_list<T>) {} template<typename F, std::size_t... S> constexpr void static_for(F && function, std::index_sequence<S...>) { return static_consume({ (function(std::integral_constant<std::size_t, S>{}), 0)... }); }Cohligan
@Cohligan you can post this as an answer if you wish. I don't think such code is worth doing in C++11 because the lack of generic lambda and C++14 allows declarations in constexpr function. It may still help someone stuck with C++11 though.Eleanor
B
8

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.

Blather answered 2/2, 2017 at 15:4 Comment(0)
C
1

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
Cohligan answered 17/2, 2018 at 11:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.