8

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;
}
Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • 1
    I see no `if constexpr` in your code.. – Jesper Juhl Feb 21 '18 at 18:36
  • @JesperJuhl Ugh, thanks, I meant to give an example of how I'd like to use a `for constexpr` – Jonathan Mee Feb 21 '18 at 18:37
  • https://stackoverflow.com/questions/45758545/unroll-loop-at-compile-time https://stackoverflow.com/questions/6872919/compile-time-loops Does the answers of those questions help you, or do you want to use explicitly the for statement for looping? – Sebi Feb 21 '18 at 18:51
  • 4
    No, there is no `for constexpr`. I saw [this discussion over it over on the std-proposals google group](https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/A9Swn2Ub2qg), though – Justin Feb 21 '18 at 18:58

3 Answers3

10

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), ...); }
max66
  • 65,235
  • 10
  • 71
  • 111
  • 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. – Jonathan Mee Feb 21 '18 at 19:07
  • 1
    @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. – max66 Feb 21 '18 at 19:16
6

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.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • 1
    "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. – glibg10b Jul 06 '22 at 09:22
3

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524