constexpr in for-Statement
Asked Answered
C

4

10

provides if constexpr, in which:

the value of condition must be a contextually converted constant expression of type bool. If the value is true, then statement-false is discarded (if present), otherwise, statement-true is discarded

Is there a way to use this in a for-statement as well? To unroll a loop at compile time? Id like to be able to do something like this:

template <int T>
void foo() {
   for constexpr (auto i = 0; i < T; ++i) cout << i << endl;
}
Cymbal answered 21/2, 2018 at 18:34 Comment(4)
I see no if constexpr in your code..Wherefrom
@JesperJuhl Ugh, thanks, I meant to give an example of how I'd like to use a for constexprCymbal
#45759045 https://mcmap.net/q/488745/-compile-time-loops Does the answers of those questions help you, or do you want to use explicitly the for statement for looping?Beget
No, there is no for constexpr. I saw this discussion over it over on the std-proposals google group, thoughTrigonous
R
11

Is there a way to use this in a for-statement as well? To unroll a loop at compile time? Id like to be able to do something like this

I don't think in a so simple way.

But if you can afford an helper function, with std::integer_sequence and the initialization of an unused C-style array of integers, starting from C++14 you can do something as follows

#include <utility>
#include <iostream>

template <int ... Is>
void foo_helper (std::integer_sequence<int, Is...> const &)
 {
   using unused = int[];

   (void)unused { 0, (std::cout << Is << std::endl, 0)... };
 }

template <int T>
void foo ()
 { foo_helper(std::make_integer_sequence<int, T>{}); }

int main ()
 {
   foo<42>();
 }

If you can use C++17, you can avoid the unused array and, using folding, foo_helper() can be simply written as follows

template <int ... Is>
void foo_helper (std::integer_sequence<int, Is...> const &)
 { ((std::cout << Is << std::endl), ...); }
Ramos answered 21/2, 2018 at 18:57 Comment(2)
Well... I wasn't really looking for a way to improve my toy example, but good gracious, this is fascinating. If you can explain what unused is doing here it's worth a +1 from me.Cymbal
@JonathanMee - In the meaning I've added a C++17 simplification. Anyway... unused is simply a C-style array that is declared only to be initialized with zeros. But using the power of the comma operator, you can add the std::cout part before the comma so std::cout is executed but doesn't affect the values of the array. In other words, unused is just a trick to provide an environment where to unpack the Is... values.Ramos
P
5

If loop limits are known to the compiler, compiler will unroll the loop, if it will find it beneficial. Not all loop unrolls are beneficial! And it is unlikely you will make a better decision than compiler as to benefits of loop unrolling.

There is no need for constexpr for (as you put it), since constexpr if is enabling feature - you may put code which would make program ill-formed inside constexpr if false branch - it is not pure optimization.

Constexpr for, on the other hand, would be pure optimization (at least as you describe it, not counting the fringe case of loop executed 0 times) and as such is better left to 'as-if' optimization rules.

Pas answered 21/2, 2018 at 18:54 Comment(2)
"Constexpr for, on the other hand, would be pure optimization" If constexpr for existed, the loop variable would be usable in constant expressions such as a template argument lists and consteval functions.Saxe
A constexpr for allows you to use the index (or the value) as a constant. Ie, you can do a constexpr for where the index is used as an array size, or similar. A classic example of its use is to populate a runtime table with a sequence of different compile time functions whose behavior is computed at compile time for later dispatch; today I'd use a variant of tags for that. But the case where you need full iteration is also possible.Fiddle
F
4

Not without helper code.

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;

template<std::size_t...Is>
constexpr auto index_over( std::index_sequence<Is...> ) noexcept(true) {
  return [](auto&& f)
    RETURNS( decltype(f)(f)( index_t<Is>{}... ) );
}
template<std::size_t N>
constexpr auto index_upto( index_t<N> ={} ) noexcept(true) {
  return index_over( std::make_index_sequence<N>{} );
}

template<class F>
constexpr auto foreacher( F&& f ) {
  return [&f](auto&&...args) noexcept( noexcept(f(args))&&... ) {
    ((void)(f(args)),...);
  };
}

that is our plumbing.

template<int T>
void foo() {
  index_upto<T>()(
    foreacher([](auto I){
      std::cout << I << "\n";
    })
  );
}

a compile time loop with values on each step.

Or we can hide the details:

template<std::size_t start, std::size_t length, std::size_t step=1, class F>
constexpr void for_each( F&& f, index_t<start> ={}, index_t<length> ={}, index_t<step> ={} ) {
  index_upto<length/step>()(
    foreacher([&](auto I){
      f( index_t<(I*step)+start>{} );
    })
  );
}

then we'd get:

for_each<0, T>([](auto I) {
  std::cout << I << "\n";
});

or

for_each([](auto I) {
  std::cout << I << "\n";
}, index_t<0>{}, index_t<T>{});

this can be further improved with user defined literals and template variables:

template<std::size_t I>
constexpr index_t<I> index{};

template<char...cs>
constexpr auto operator""_idx() {
  return index< parse_value(cs...) >;
}

where parse_value is a constexpr function that takes a sequence of char... and produces an unsigned integer representation of it.

Fiddle answered 21/2, 2018 at 19:51 Comment(2)
decltype(f)(f) – I think I’ve seen this somewhere else too; could you explain the purpose of that?Lebensraum
@Lebensraum It is a pseudo-std-forward. I read it as f as the type it was declared as. It only works for auto&& type variables; otherwise, it can induce a copy. It is equivalent in that case to std::forward<decltype(f)>(f).Fiddle
R
0

As an alternative to @Yakk's answer, https://artificial-mind.net/blog/2020/10/31/constexpr-for gives a much simpler solution. This requires c++17

template <auto Start, auto End, auto Inc = 1, typename F>
constexpr void constexpr_for(F&& f) {
    static_assert(Inc != 0);
    if constexpr ((Inc > 0 && Start < End) || (Inc < 0 && End < Start)) {
        f(std::integral_constant<decltype(Start), Start>());
        constexpr_for<Start + Inc, End, Inc>(f);
    }
}

use:

constexpr_for<0,5>([&](auto i) { dostuff(i); });
//0,1,2,3,4

You can even alter it like so, so that it auto-deduces the begin and end.

template <auto Start, auto End = Start, auto Inc = 0, typename F>
constexpr void constexpr_for(F&& f) {
    //sanity check for params
    static_assert((Inc == 0) || (Inc > 0 && End > Start) || (Inc < 0 && Start > End)); 
    //deduce direction from start and end if 2 params given
    constexpr auto dir = (Inc != 0) ? Inc: (Start > End) ? -1 : 1; 
    //deduce first & last if only 1 param given
    constexpr auto first = (Start == End) ? 0 : Start;
    constexpr auto last = (Start == End) ? Start : End;
    //next simplifies code later
    constexpr auto next = first + dir;
    //while not done, do loop_work
    if constexpr ((dir > 0 && first < last) || ((dir < 0 && last < first))) {
        //integral_constant makes first a constant expression
        //calling `f(first)` gives error: "expr. must have constant value" 
        f(std::integral_constant<decltype(Start), first>());
        //if the next loop will be valid...
        if constexpr ((dir > 0 && next < last) || (dir < 0 && last < next)) {
            //... run one more loop
            constexpr_for<next, last, dir>(f);
        }
    }
}

use:

constexpr_for<4,-1>([&](auto i) { dostuff(i); }); // 4 3 2 1 0
constexpr_for<5>([&](auto i) {dostuff(i); }); //0 1 2 3 4

If you want to iterate over a parameter pack, overload the constexpr_for as follows:

template <typename F, typename... Args>
constexpr void constexpr_for(F&& f, Args&&... args) {
    (f(std::forward<Args>(args)), ...);
}

use:

template <class... Args>
void print_all(Args const&... args) {
    constexpr_for([](auto const& v) {
        std::cout << v << std::endl;
    }, args...);
}

Further reading: https://vittorioromeo.info/index/blog/cpp20_lambdas_compiletime_for.html

Riproaring answered 15/9, 2024 at 14:43 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.