Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to initialise a floating point array at compile time?

I have found two good approaches to initialise integral arrays at compile times here and here.

Unfortunately, neither can be converted to initialise a float array straightforward; I find that I am not fit enough in template metaprogramming to solve this through trial-and-error.

First let me declare a use-case:

constexpr unsigned int SineLength  = 360u;
constexpr unsigned int ArrayLength = SineLength+(SineLength/4u);
constexpr double PI = 3.1415926535;

float array[ArrayLength];

void fillArray(unsigned int length)
{
  for(unsigned int i = 0u; i < length; ++i)
    array[i] = sin(double(i)*PI/180.*360./double(SineLength));
}

As you can see, as far as the availability of information goes, this array could be declared constexpr.

However, for the first approach linked, the generator function f would have to look like this:

constexpr float f(unsigned int i)
{
  return sin(double(i)*PI/180.*360./double(SineLength));
}

And that means that a template argument of type float is needed. Which is not allowed.

Now, the first idea that springs to mind would be to store the float in an int variable - nothing happens to the array indices after their calculation, so pretending that they were of another type than they are (as long as byte-length is equal) is perfectly fine.

But see:

constexpr int f(unsigned int i)
{
  float output = sin(double(i)*PI/180.*360./double(SineLength));
  return *(int*)&output;
}

is not a valid constexpr, as it contains more than the return statement.

constexpr int f(unsigned int i)
{
  return reinterpret_cast<int>(sin(double(i)*PI/180.*360./double(SineLength)));
}

does not work either; even though one might think that reinterpret_cast does exactly what is needed here (namely nothing), it apparently only works on pointers.

Following the second approach, the generator function would look slightly different:

template<size_t index> struct f
{
  enum : float{ value = sin(double(index)*PI/180.*360./double(SineLength)) };
};

With what is essentially the same problem: That enum cannot be of type float and the type cannot be masked as int.


Now, even though I have only approached the problem on the path of "pretend the float is an int", I do not actually like that path (aside from it not working). I would much prefer a way that actually handled the float as float (and would just as well handle a double as double), but I see no way to get around the type restriction imposed.

Sadly, there are many questions about this issue, which always refer to integral types, swamping the search for this specialised issue. Similarly, questions about masking one type as the other typically do not consider the restrictions of a constexpr or template parameter environment.
See [1][2][3] and [4][5] etc.

like image 674
Zsar Avatar asked Dec 16 '15 11:12

Zsar


People also ask

How do you initialize an array at compile time?

Using runtime initialization user can get a chance of accepting or entering different values during different runs of program. It is also used for initializing large arrays or array with user specified values. An array can also be initialized at runtime using scanf() function.

What is compile time initialization?

With the MicroC profile, you can specify that elements should be initialized at compile time. Compile-time initialization provides the following benefits: Ability to allocate data to ROM. Saving of CPU cycles at application startup. Ability to allocate data to specific memory segments.


1 Answers

Assuming your actual goal is to have a concise way to initialize an array of floating point numbers and it isn't necessarily spelled float array[N] or double array[N] but rather std::array<float, N> array or std::array<double, N> array this can be done.

The significance of the type of array is that std::array<T, N> can be copied - unlike T[N]. If it can be copied, you can obtain the content of the array from a function call, e.g.:

constexpr std::array<float, ArrayLength> array = fillArray<N>();

How does that help us? Well, when we can call a function taking an integer as an argument, we can use std::make_index_sequence<N> to give use a compile-time sequence of std::size_t from 0 to N-1. If we have that, we can initialize an array easily with a formula based on the index like this:

constexpr double const_sin(double x) { return x * 3.1; } // dummy...
template <std::size_t... I>
constexpr std::array<float, sizeof...(I)> fillArray(std::index_sequence<I...>) {
    return std::array<float, sizeof...(I)>{
            const_sin(double(I)*M_PI/180.*360./double(SineLength))...
        };
}

template <std::size_t N>
constexpr std::array<float, N> fillArray() {
    return fillArray(std::make_index_sequence<N>{});
}

Assuming the function used to initialize the array elements is actually a constexpr expression, this approach can generate a constexpr. The function const_sin() which is there just for demonstration purpose does that but it, obviously, doesn't compute a reasonable approximation of sin(x).

The comments indicate that the answer so far doesn't quite explain what's going on. So, let's break it down into digestible parts:

  1. The goal is to produce a constexpr array filled with suitable sequence of values. However, the size of the array should be easily changeable by adjusting just the array size N. That is, conceptually, the objective is to create

    constexpr float array[N] = { f(0), f(1), ..., f(N-1) };
    

    Where f() is a suitable function producing a constexpr. For example, f() could be defined as

    constexpr float f(int i) {
        return const_sin(double(i) * M_PI / 180.0 * 360.0 / double(Length);
    }
    

    However, typing in the calls to f(0), f(1), etc. would need to change with every change of N. So, essentially the same as the above declaration should be done but without extra typing.

  2. The first step towards the solution is to replace float[N] by std:array<float, N>: built-in arrays cannot be copied while std::array<float, N> can be copied. That is, the initialization could be delegated to to a function parameterized by N. That is, we'd use

    template <std::size_t N>
    constexpr std::array<float, N> fillArray() {
        // some magic explained below goes here
    }
    constexpr std::array<float, N> array = fillArray<N>();
    
  3. Within the function we can't simply loop over the array because the non-const subscript operator isn't a constexpr. Instead, the array needs to be initialized upon creation. If we had a parameter pack std::size_t... I which represented the sequence 0, 1, .., N-1 we could just do

    std::array<float, N>{ f(I)... };
    

    as the expansion would effectively become equivalent to typing

    std::array<float, N>{ f(0), f(1), .., f(N-1) };
    

    So the question becomes: how to get such a parameter pack? I don't think it can be obtained directly in the function but it can be obtained by calling another function with a suitable parameter.

  4. The using alias std::make_index_sequence<N> is an alias for the type std::index_sequence<0, 1, .., N-1>. The details of the implementation are a bit arcane but std::make_index_sequence<N>, std::index_sequence<...>, and friends are part of C++14 (they were proposed by N3493 based on, e.g., on this answer from me). That is, all we need to do is call an auxiliary function with a parameter of type std::index_sequence<...> and get the parameter pack from there:

    template <std::size_t...I>
    constexpr std::array<float, sizeof...(I)>
    fillArray(std::index_sequence<I...>) {
        return std::array<float, sizeof...(I)>{ f(I)... };
    }
    template <std::size_t N>
    constexpr std::array<float, N> fillArray() {
        return fillArray(std::make_index_sequence<N>{});
    }
    

    The [unnamed] parameter to this function is only used to have the parameter pack std::size_t... I be deduced.

like image 137
Dietmar Kühl Avatar answered Sep 30 '22 14:09

Dietmar Kühl