c++c++17constexprlookup-tables

Creating a compile-time calculated Lookup Table that uses math functions to calculate the elements


I am trying to create a LUT in C++ that is calculated at compile-time and is simply placed in memory for other modules to use. Every term in this LUT is calculate-able at compile time, it's just a thick mess of constants and operations such as sums, exponents, and spherical bessel functions.

Example calculation that would make up one of the elements. Note again, that every single piece of the equation is known at compile time. I would just like the compiler to do the work for me and generate a LUT.

Coming exclusively from an embedded C background, I decided to interface with a C++ module to utilize constexpr capability, which my research showed me would be the right mechanism to generate a fairly large 2D lookup table without resorting to hundreds of ugly array initializer statements. My thought was: I could write some functions, mark them constexpr and all structs, variables, and functions they touch as constexpr and everything would work out.

The issue I'm running into is that the calculation for elements in this LUT are calculated using c++ stl math functions like std::powf() or std::cyl_bessel_jf() and these functions are not constexpr. They use the errnoglobal status variable in a non-thread safe manner and are thus non-reentrant and therefore cannot be constexpr. Cool.

But that shouldn't matter for my use case. It is irrelevant whether the target application, at run-time, has a thread-safe implementation of std::powf() I want the compiler to just compute those at compile-time and put it into memory!

Which leads me to my question: is constexpr not the C++ mechanism for this, or am I doing something syntactically stupid with my code?

A potential footgun here is that I'm calling foo() from a different, C module, not C++.

Code that illustrates the example without going into too much math muck. Note: I am using C++17 with gcc


constexpr int t_cnt = 100;
constexpr int r_cnt = 100;

constexpr float _v_param  = 1.0f;
constexpr float _R_param  = 1.0f;
constexpr float _mu_param = 1.0f;

constexpr float exp_func (float t, float r)
{
    return expf(-1.0f * powf(r, 2) * _v_param * t / powf(_R_param, 2));
}

struct LUT
{
    constexpr LUT() : values()
    {
        for (auto t = 0; t < t_cnt; t++)
        {
            for (auto r = 0; r < r_cnt; r++)
            {
                values[t][r] = exp_func(t, r);
            }
        }
    }

    float  values[t_cnt][r_cnt];
};

void foo ()
{
    constexpr auto footable = LUT();
    /*I would like to use footable in other parts of the code,
      to get values like footable[0][1] without calculating it in runtime*/
}

This code generates the following error:

src/precompute.cpp: In function 'void foo()':
src/precompute.cpp:143:30:   in 'constexpr' expansion of 'LUT()'
src/precompute.cpp:133:40:   in 'constexpr' expansion of 'exp_func((float)LUT::__ct ::t, (float)LUT::__ct ::r)'
src/precompute.cpp:122:16: error: 'expf(-1.0e+2f)' is not a constant expression
  122 |     return expf(-1.0f * powf(r, 2) * _v_param * t / powf(_R_param, 2));

I expect that I should be able to create the LUT struct at compile time, and simply read values from it.


Solution

  • Which leads me to my question: is constexpr not the C++ mechanism for this, or am I doing something syntactically stupid with my code?

    You are not doing anything wrong and you are also correct that the part of the functionality of the math functions that you want to use in principle also works in constant expressions.

    The issue is simply that these functions originally weren't written to be used at compile-time and one needs to do work to specify how they should behave at compile-time (e.g how to handle errno, how to handle floating point exceptions) and then the library implementers need to implement these specifications.

    That's true for all library functionality, not only math functions. constexpr has been introduced in C++11 and since then successively more and more of the core language and the library has been made usable at compile-time. For the math functions this work has also started with C++23 and some more functions will be constexpr in C++26.

    You can always write these functions (with the behavior that you want) yourself as constexpr and use them. There is nothing fundamentally problematic about it once you have decided on the errno and floating point exception behavior.

    An implementation problem before C++20 was that there was no syntax in the core language to allow for different implementations of the same function at compile-time and at runtime. But for the math functions you likely need this, e.g. to switch writing errno or to switch between a C++ implementation at compile-time and a more performant assembler implementation at runtime.

    A potential footgun here is that I'm calling foo() from a different, C module, not C++.

    That is not an issue. Nothing will go wrong when you do that.