0

Language & standard: C++17

What I hope to achieve: I have created a global array, say int List[someconstant], which is constexpr constructed ; as such, I can pass its entries as template parameters, or to other constexpr functions and variables.

Now, I have a for loop such as :

for(int i = 0; i < __MYCONST__ ; i++){
    int j = List[i];
    function<j>(/*some data declared outside the loop*/);
}

where __MYCONST__ is known at compile time (constexpr global). I know this to be true as I have already successfully passed __MYCONST__ as a template parameter elsewhere in the code. I would now like to write

constexpr int j = List[i];

or directly

function<List[i]>();

but this fails as i is considered a variable. It is not modified within the loop and the bounds are known at compile time. Thus I believe there should be a way to get that constexpr int j.

  1. What could I replace the for loop construct with in order to be able to declare j constexpr?
  2. Or is there a way to call a templated function whose template parameters depend on i (through constexpr arrays) which is "weaker" than declaring j constexpr and passing j as the template parameter?

Notes: passing List[i] as a function argument instead of template parameter is not an option.

I have successfully created a "constexpr_loop" function:

template <int i, int N, typename FunctorType> 
struct  basic_recursive{
    basic_recursive(FunctorType fun){
        fun(i);
        basic_recursive<i+1,N,FunctorType> a(fun);
    };
};
template <int N, typename FunctorType> 
struct basic_recursive<N,N,FunctorType>{
    basic_recursive(FunctorType fun){};
};

template<int i, int N, typename FunctorType>
void constexpr_for(FunctorType fun){
    basic_recursive<i,N,FunctorType> a(fun);
}

It is then called as constexpr_for([&](int i) {loop body})

This is a bit convoluted but it works as follows:

  • the templated function constexpr_for handles template parameter deduction, which templated classes or structs cannot do.
  • the templated structs handle termination with partially specialized instances, which function templates cannot do

So it's a bit of a "ping-pong" between templated functions and structs, which have complementary permissions with regards to deduction/specialization.

Now, if you think the above could be adapted to handle templated "FunctorType" with a non-type parameter, that would work; in that case, the template parameter would be the loop index and, instead of doing fun(i); I could do fun<i>(); in the body of the basic_recursive constructor. I have tried compiling with -std=c++20 and using templated lambdas but I can't seem to make template parameter deduction work when one of the template parameters is itself a non-type templated type.

I have searched quite a bit to solve this problem and experimented with other solutions but they didn't seem more promising. The above code at least compiles, and is almost where I want it. I'm a beginner in C++ so I may have made mistakes exposing some aspects of the problem or the attempted solution.

-- EDIT: MWE

The furthest I've gone is as follows. Instead of the above structs and functions, define:

template <int i, int N, typename FunctorType> 
struct  basic_recursive2{
  basic_recursive2(FunctorType fun){ 
      fun();
      basic_recursive2<i+1,N,FunctorType> a(fun);
  };
};
template <int N, typename FunctorType> 
struct basic_recursive2<N,N,FunctorType>{
  basic_recursive2(FunctorType fun){};
};

template<int i, int N, template<int> typename FunctorType>
void constexpr_for2(FunctorType<i> fun){
  basic_recursive2<i,N,FunctorType<i>> a(fun);
}

In the main, we have

constexpr_for2<1,3>([&]<int i>{printf("print in temp lambda %d \n",i);});

The main difference is that the function that is supposed to capture the type FunctorType now expects FunctorType to be a template<int> typename, instead of simply a typename. It then instantiates the template by declaring fun to be a FunctorType<i> instead of a FunctorType. The recursive structs take a simple typename as template parameter, which stands for the instantiated FunctorType<i>, which is not longer a templated typename but a simple typename. Finally, the struct constructor calls fun(), which already has i as a template argument, rather than fun(i). Note: this is not satisfactory yet because I want i to be passed known by the structs so that it may be changed from call to call. But this already doesn't compile:

file.cxx: error: no matching function for call to 'constexpr_for2'
  constexpr_for2<1,3>([&]<int i>{printf("print in temp lambda %d \n",i);});
  ^~~~~~~~~~~~~~~~~~~
file.cxx: note: candidate template ignored: could not match 'FunctorType<1>' against '(lambda at file.cxx)'
void constexpr_for2(FunctorType<i> fun){
     ^
file.cxx: note: candidate template ignored: invalid explicitly-specified argument for template parameter 'FunctorType'
void constexpr_for2(FunctorType<i> fun){
     ^

-------------- Solution

@user17732522 proposed the following solution:

[&]<int... j>(std::integer_sequence<int, j...>){
 (function<List[j]>    (/*some data declared outside the loop*/), ...); 
}(std::make_integer_sequence<int, __MYCONST__>{});

They have not explained it further yet, but I believe this defines a templated lambda and immediately calls it on an std::make_integer_sequence which is formed at compile time as <int, 1, ..., MYCONST>. This answer taught me about parameter pack expansion, as this is the crucial bit at work. When writing (func<j>(), ...), this is expanded to func<1>(), func<2(), .... The rest seems to be that the giving the lambda the template parameter as a dummy argument allows for template parameter deduction when calling the lambda.

I then adapted this answer to work for a loop body that has several statements. I didn't want to define an external function, so I created yet another lambda, as follows:

[&]<int... j>(std::integer_sequence<int, j...>){
 ( [&]<int i>(){
  instruction 1 depending on i;
  instruction 2 depending on i;...
}.template operator()<j>(),...);
}(std::make_integer_sequence<int, __MYCONST__>{});

Basically, the innermost lambda is templated, and we use the parameter list expansion outside of it, rather than within a statement.

This was a huge boost in performance, as templating for all loop bounds (or their indices in constexpr arrays) in the routines called by this loop provided between x2.5 to x4 speedup depending on function call parameters.

Sardine
  • 153
  • 5
  • 1
    Post a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) with complete error instead of describing the error in your own words. –  May 16 '23 at 17:39
  • 1
    `__CONSTANT__` is a reserved identifier. Technically the code has undefined behavior https://en.cppreference.com/w/cpp/language/identifiers – 463035818_is_not_an_ai May 16 '23 at 17:39
  • 1
    "the templated function constexpr_for handles template parameter deduction, which templated classes or structs cannot do." since c++17 there is class template argument deduction (https://en.cppreference.com/w/cpp/language/class_template_argument_deduction) – 463035818_is_not_an_ai May 16 '23 at 17:42
  • 1
    C++20: `[&](std::integer_sequence){ (function(/*some data declared outside the loop*/), ...); }(std::make_integer_sequence{});` – user17732522 May 16 '23 at 17:43
  • 1
    what is the underlying problem you need to solve? Compile time loops is an idea that pops up from time to time in questions, but its not something needed frequently, so it could be that there is a much simpler way to solve your actual problem – 463035818_is_not_an_ai May 16 '23 at 17:44
  • https://stackoverflow.com/questions/6872919/compile-time-loops – 463035818_is_not_an_ai May 16 '23 at 17:48
  • https://stackoverflow.com/questions/48913092/constexpr-in-for-statement?noredirect=1&lq=1 – 463035818_is_not_an_ai May 16 '23 at 17:48
  • @user17732522 Thank you so much, that does seem to work! Would you be willing to explain that solution in some more detail in an answer to this question? From what I understand, the bulk of that is declaring a templated lambda function that takes a variadic template parameter list of integers. But isn't std::integer_sequence a template parameter list itself? How can that be a function argument? And what does (func(), ...) mean in the body? – Sardine May 16 '23 at 23:47
  • @463035818_is_not_a_number Indeed, I have modified my example code to avoid the named constant. I used the format to signify a macro-defined constant, but it could have been any const or constexpr integer. – Sardine May 16 '23 at 23:50
  • @463035818_is_not_a_number I'm not entirely sure this will be useful, but I have a small routine parameterized by integers, which currently evaluates the useful quantities from these integers at runtime, with lots of tiny loops (something like 20 loops from anywhere between 0 and 0 to 0 and 2). This is 10 times slower than the hand-expanded version, which simplifies tremendously. I'm attempting to help the compiler optimize this by propagating known information as deep as possible into the routine, here by templating the loop bound arguments (or indices to fetch the bounds in const arrays). – Sardine May 16 '23 at 23:53
  • @463035818_is_not_a_number Regarding the proposed links, what I understand of the first is that the answers implement what I already have, but indeed the second seems to provide what I seek, and is similar to the answer proposed by user17732522. Thank you for these references. – Sardine May 16 '23 at 23:56

2 Answers2

1

If I understand your requirement correctly, you need to invoke a templated function with template arguments from the elements of a constexpr array. This is the simplest implementation I could achieve. The trick is passing the std::array as a template parameter in order to invoke the target function using the elements in turn with a fold expression.

Code

#include <array>
#include <iostream>

template<size_t N>
void target_function() {
    std::cout << N << std::endl;
}

template<class T, size_t N, std::array<T, N> Arr, size_t... Is>
void indirect_function(std::index_sequence<Is...>) {
    (target_function<Is>(), ...);
}

int main()
{
    constexpr std::array<int, 3> arr{1, 2, 3};
    indirect_function<int, 3, arr>(std::make_index_sequence<arr.size()>{});
    return 0;
}

Output

0
1
2
RandomBits
  • 4,194
  • 1
  • 17
  • 30
  • Thanks, that would work, I believe. Since yesterday, I've applied the solution @user17732522 proposed in a comment (similar to yours, using parameter pack expansion and std::index_sequence too) and adapted it to my case, where I have multiple instructions in the loop body (I had to define another lambda, and enclose it in the parameter pack expansion). There's also the issue that I simplified in the exposure, but I have several arrays to grab entries in at `i`. Still, I think your answer addresses the question as I posed it and may be useful to future readers. – Sardine May 17 '23 at 12:35
1

Both @user17732522 in comments and @RandomBits in their answer proposed solutions using std::integer_sequence<typename, int N> which expands to a list <typename, 1, ...., N>. I adapted @user17732522's solution to my problem as it was written first:

[&]<int... j>(std::integer_sequence<int, j...>){ 
(function<List[j]>(/*some data declared outside the loop*/), ...);
}(std::make_integer_sequence<int, __CONSTANT__>{});

I think I can explain this solution now that I have tinkered with it. I have also had to adapt it to a more realistic case with several statements, which could prove useful to readers looking to speed loops up. The bulk of it is a templated lambda function [&]<int ...j>(...){...} which takes a pack of ints as its parameter.

Template parameter passing

In order to populate the template parameters on function call, they used parameter deduction by function call, using the same j as a function argument, like so: [&]<int ...j>(something of j){...}. This something is an std::integer_sequence, created by std::make_integer_sequence<int, N> to hold N ints. The curly brackes, if I understand correctly, instantiate an object of the class std::make_integer_sequence<int, N> to be passed as an argument.

Iterating over template parameters

The second task of this lambda is to iterate over those template parameters. This is done using parameter pack expansion, as follows: (func(j),...), where j is the pack of ints. In this context, the compiler treats this syntax as (func(1), func(2), ..., func(N)), so this is really what I was looking for: compile-time iteration.

Going further: multiple statements

In my exposition, I mentioned one instruction in the loop body, but most realistic applications of this technique will have several. Most likely, they must be carried out in order, so they cannot be put each in their own pack expansion, as that would be equivalent to writing several for loops where there was one. To adapt the solution, I introduced a second templated lambda, whose call is the one inside a pack expansion:

[&]<int... j>(std::integer_sequence<int, j...>){ 
  ( [&]<int i>(){
    // Loop body with many statements that depend on i
  }.template operator()<j>(),...);
}(std::make_integer_sequence<int, __CONSTANT__>{});

The sequence template operator()<j>() invockes the lambda's () operator (function call) instantiated to the integer j coming from the parameter pack.

Sardine
  • 153
  • 5