【问题标题】:How to initialise a floating point array at compile time?如何在编译时初始化浮点数组?
【发布时间】:2016-03-22 12:30:24
【问题描述】:

我发现了两种在编译时初始化整数数组的好方法herehere

不幸的是,两者都不能直接转换为初始化浮点数组;我发现我不适合模板元编程通过反复试验来解决这个问题。

首先让我声明一个用例:

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));
}

如您所见,就信息的可用性而言,这个数组可以声明为constexpr

但是,对于链接的第一种方法,生成器函数 f 必须如下所示:

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

这意味着需要float 类型的模板参数。这是不允许的。

现在,想到的第一个想法是将浮点数存储在一个 int 变量中 - 数组索引在计算后没有任何反应,所以假装它们是另一种类型(只要 byte-长度相等)完全没问题。

但请看:

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

不是有效的constexpr,因为它包含的不仅仅是返回语句。

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

也不行;尽管有人可能认为reinterpret_cast 完全符合这里的要求(即什么都没有),但它显然只适用于指针。

按照第二种方法,生成器函数看起来会略有不同:

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

本质上是相同的问题:该枚举不能是 float 类型,并且该类型不能被屏蔽为 int


现在,即使我只在“假装floatint”的路径上解决了问题,我实际上并不喜欢这条路径(除了它不起作用)。我更喜欢一种将float 实际处理为float 的方式(也可以将double 处理为double),但我认为没有办法绕过施加的类型限制。

遗憾的是,关于这个问题有很多问题,它们总是提到整数类型,淹没了对这个专业问题的搜索。同样,关于将一种类型屏蔽为另一种类型的问题通常不会考虑 constexpr 或模板参数环境的限制。
[1][2][3][4][5]等。

【问题讨论】:

  • 您不会很幸运地基于std::sin() 创建constexpr 函数,因为此函数不是constexpr 函数。也许应该是,但目前还不是。
  • @DietmarKühl 这是某些编译器的内在特性。此外,实现系列扩展并不难。
  • 为什么不使用 Excel 做数学运算并编写一个宏来吐出数组的源代码?
  • @DietmarKühl 谁说代码应该是可移植的?

标签: c++ arrays templates c++14 constexpr


【解决方案1】:

假设您的实际目标是有一种简洁的方法来初始化浮点数数组,并且不一定拼写为float array[N]double array[N],而是std::array&lt;float, N&gt; arraystd::array&lt;double, N&gt; array 这可以做到。

数组类型的意义在于std::array&lt;T, N&gt;可以被复制——不像T[N]。如果可以复制,可以通过函数调用获取数组的内容,例如:

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

这对我们有什么帮助?好吧,当我们可以调用一个以整数作为参数的函数时,我们可以使用std::make_index_sequence&lt;N&gt; 来给出从0N-1std::size_t 的编译时序列。如果有,我们可以使用基于索引的公式轻松初始化数组,如下所示:

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>{});
}

假设用于初始化数组元素的函数实际上是一个constexpr表达式,这种方式可以生成一个constexpr。仅用于演示目的的函数 const_sin() 可以做到这一点,但它显然不能计算出 sin(x) 的合理近似值。

cmets 表明到目前为止的答案并不能完全解释发生了什么。所以,让我们把它分解成可消化的部分:

  1. 目标是生成一个constexpr 数组,其中填充了合适的值序列。但是,只需调整数组大小N,就可以轻松更改数组的大小。也就是说,从概念上讲,目标是创建

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

    其中f() 是产生constexpr 的合适函数。例如,f() 可以定义为

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

    但是,键入对f(0)f(1) 等的调用将需要随着N 的每次更改而更改。因此,应该与上述声明基本相同,但无需额外输入。

  2. 解决方案的第一步是将float[N] 替换为std:array&lt;float, N&gt;:无法复制内置数组,而可以复制std::array&lt;float, N&gt;。也就是说,初始化可以委托给N 参数化的函数。也就是说,我们会使用

    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. 在函数中,我们不能简单地循环数组,因为非const 下标运算符不是constexpr。相反,数组需要在创建时进行初始化。如果我们一个参数包std::size_t... I代表序列0, 1, .., N-1,我们可以

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

    因为扩展实际上等同于打字

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

    那么问题就变成了:如何得到这样的参数包?我不认为可以直接在函数中获取,但是可以通过调用另一个具有合适参数的函数来获取。

  4. 使用别名std::make_index_sequence&lt;N&gt; 是类型std::index_sequence&lt;0, 1, .., N-1&gt; 的别名。实现的细节有点神秘,但std::make_index_sequence&lt;N&gt;std::index_sequence&lt;...&gt; 和朋友都是 C++14 的一部分(它们是由N3493 基于例如this answer from me 提出的)。也就是说,我们需要做的就是调用一个带有std::index_sequence&lt;...&gt;类型参数的辅助函数,并从那里获取参数包:

    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>{});
    }
    

    此函数的[未命名]参数仅用于推导参数包std::size_t... I

【讨论】:

  • 您可以在 Coliru 或 Wandbox 上试用您的代码。另外,这真的是在回答提问者的问题吗?除非 sin 是内在的并且编译器决定这样做,否则您不能保证上述内容的静态初始化。
  • @Columbo:有时在工作场所可以做的事情会受到限制。
  • @Columbo:静态初始化不是常量初始化。它发生在运行时不断初始化之后。最初的问题可能意味着不断的初始化。此外,由于不能保证sin()constexpr,因此无论如何都不能保证使用此函数进行常量初始化。如果所有涉及的操作都是constexpr,这也适用于常量初始化。
  • 我用可以编译的东西编辑了代码。如果您不喜欢它,请随意回滚。
  • 顺便说一句,在 C++14 中,在 consexpr 的情况下,一个简单的循环也可以工作(而不是 index_sequence)。
【解决方案2】:

这是一个生成 sin 值表的工作示例,您可以通过传递不同的函数对象轻松适应对数表

#include <array>    // array
#include <cmath>    // sin
#include <cstddef>  // size_t
#include <utility>  // index_sequence, make_index_sequence
#include <iostream>

namespace detail {

template<class Function, std::size_t... Indices>
constexpr auto make_array_helper(Function f, std::index_sequence<Indices...>)
        -> std::array<decltype(f(std::size_t{})), sizeof...(Indices)>
{
        return {{ f(Indices)... }};
}

}       // namespace detail

template<std::size_t N, class Function>
constexpr auto make_array(Function f)
{
        return detail::make_array_helper(f, std::make_index_sequence<N>{});
}

static auto const pi = std::acos(-1);
static auto const make_sin = [](int x) { return std::sin(pi * x / 180.0); };
static auto const sin_table = make_array<360>(make_sin);

int main() 
{
    for (auto elem : sin_table)
        std::cout << elem << "\n";
}

Live Example.

请注意,您需要使用-stdlib=libc++,因为libstdc++index_sequence 的实现效率非常低。

还请注意,您需要一个 constexpr 函数对象(pistd::sin 都不是 constexpr)在编译时而不是在程序初始化时真正初始化数组。

【讨论】:

  • 对于一个在编译时(应该被)消除并因此永远不会在正在运行的程序中计算的表达式,实现效率如何显示?使其进入编译程序的唯一部分将是数组本身,不是吗?
  • @Zsar 是的,就是这个想法。
  • 嗯,好吧,但是...为什么要建议切换库,如果它会影响编译时间?
  • 澄清一下,在对this question 的第六条评论中,其他人还在constexpr 表达式的上下文中提到了“效率”。在他们的情况下(当他们写关于 constexpr functions 时)我将其解释为“如果函数必须在运行时计算,那么 那个将是低效的”。然而,在这里,由于调用图的根是 constexpr variable 的赋值,所以 all 计算必须在编译时进行。在这种情况下,“效率”一词是什么意思?
【解决方案3】:

如果你想在编译时初始化一个浮点数组,有几个问题需要克服:

  1. std::array 有点破损,因为在可变 constexpr std::array 的情况下,operator[] 不是 constexpr(我相信这将在标准的下一个版本中得到修复)。

  2. std::math 中的函数未标记为 constexpr!

我最近遇到了类似的问题域。我想创建一个准确但快速的sin(x) 版本。

我决定看看是否可以使用带有插值的constexpr 缓存来获得速度而不损失准确性。

使高速缓存 constexpr 的一个优点是 sin(x) 在编译时已知的值的计算是 sin 是预先计算的,并且只是作为立即寄存器加载存在于代码中!在运行时参数的最坏情况下,它只是一个常量数组查找,然后是 w 加权平均值。

此代码需要在 clang 上使用 -fconstexpr-steps=2000000 编译,或在 windows 中使用等效项。

享受:

#include <iostream>
#include <cmath>
#include <utility>
#include <cassert>
#include <string>
#include <vector>

namespace cpputil {

    // a fully constexpr version of array that allows incomplete
    // construction
    template<size_t N, class T>
    struct array
    {
        // public constructor defers to internal one for
        // conditional handling of missing arguments
        constexpr array(std::initializer_list<T> list)
        : array(list, std::make_index_sequence<N>())
        {

        }

        constexpr T& operator[](size_t i) noexcept {
            assert(i < N);
            return _data[i];
        }

        constexpr const T& operator[](size_t i) const noexcept {
            assert(i < N);
            return _data[i];
        }

        constexpr T& at(size_t i) noexcept {
            assert(i < N);
            return _data[i];
        }

        constexpr const T& at(size_t i) const noexcept {
            assert(i < N);
            return _data[i];
        }

        constexpr T* begin() {
            return std::addressof(_data[0]);
        }

        constexpr const T* begin() const {
            return std::addressof(_data[0]);
        }

        constexpr T* end() {
            // todo: maybe use std::addressof and disable compiler warnings
            // about array bounds that result
            return &_data[N];
        }

        constexpr const T* end() const {
            return &_data[N];
        }

        constexpr size_t size() const {
            return N;
        }

    private:

        T _data[N];

    private:

        // construct each element from the initialiser list if present
        // if not, default-construct
        template<size_t...Is>
        constexpr array(std::initializer_list<T> list, std::integer_sequence<size_t, Is...>)
        : _data {
            (
             Is >= list.size()
             ?
             T()
             :
             std::move(*(std::next(list.begin(), Is)))
             )...
        }
        {

        }
    };

    // convenience printer
    template<size_t N, class T>
    inline std::ostream& operator<<(std::ostream& os, const array<N, T>& a)
    {
        os << "[";
        auto sep = " ";
        for (const auto& i : a) {
            os << sep << i;
            sep = ", ";
        }
        return os << " ]";
    }

}


namespace trig
{
    constexpr double pi() {
        return M_PI;
    }

    template<class T>
    auto constexpr to_radians(T degs) {
        return degs / 180.0 * pi();
    }

    // compile-time computation of a factorial
    constexpr double factorial(size_t x) {
        double result = 1.0;
        for (int i = 2 ; i <= x ; ++i)
            result *= double(i);
        return result;
    }

    // compile-time replacement for std::pow
    constexpr double power(double x, size_t n)
    {
        double result = 1;
        while (n--) {
            result *= x;
        }
        return result;
    }

    // compute a term in a taylor expansion at compile time
    constexpr double taylor_term(double x, size_t i)
    {
        int powers = 1 + (2 * i);
        double top = power(x, powers);
        double bottom = factorial(powers);
        auto term = top / bottom;
        if (i % 2 == 1)
            term = -term;
        return term;
    }

    // compute the sin(x) using `terms` terms in the taylor expansion        
    constexpr double taylor_expansion(double x, size_t terms)
    {
        auto result = x;
        for (int term = 1 ; term < terms ; ++term)
        {
            result += taylor_term(x, term);
        }
        return result;
    }

    // compute our interpolatable table as a constexpr
    template<size_t N = 1024>
    struct sin_table : cpputil::array<N, double>
    {
        static constexpr size_t cache_size = N;
        static constexpr double step_size = (pi() / 2) / cache_size;
        static constexpr double _360 = pi() * 2;
        static constexpr double _270 = pi() * 1.5;
        static constexpr double _180 = pi();
        static constexpr double _90 = pi() / 2;

        constexpr sin_table()
        : cpputil::array<N, double>({})
        {
            for(int slot = 0 ; slot < cache_size ; ++slot)
            {
                double val = trig::taylor_expansion(step_size * slot, 20);
                (*this)[slot] = val;
            }
        }

        double checked_interp_fw(double rads) const {
            size_t slot0 = size_t(rads / step_size);
            auto v0 = (slot0 >= this->size()) ? 1.0 : (*this)[slot0];

            size_t slot1 = slot0 + 1;
            auto v1 = (slot1 >= this->size()) ? 1.0 : (*this)[slot1];

            auto ratio = (rads - (slot0 * step_size)) / step_size;

            return (v1 * ratio) + (v0 * (1.0-ratio));
        }

        double interpolate(double rads) const
        {
            rads = std::fmod(rads, _360);
            if (rads < 0)
                rads = std::fmod(_360 - rads, _360);

            if (rads < _90) {
                return checked_interp_fw(rads);
            }
            else if (rads < _180) {
                return checked_interp_fw(_90 - (rads - _90));
            }
            else if (rads < _270) {
                return -checked_interp_fw(rads - _180);
            }
            else {
                return -checked_interp_fw(_90 - (rads - _270));
            }
        }


    };

    double sine(double x)
    {
        if (x < 0) {
            return -sine(-x);
        }
        else {
            constexpr sin_table<> table;
            return table.interpolate(x);
        }
    }

}


void check(float degs) {
    using namespace std;

    cout << "checking : " << degs << endl;
    auto mysin = trig::sine(trig::to_radians(degs));
    auto stdsin = std::sin(trig::to_radians(degs));
    auto error = stdsin - mysin;
    cout << "mine=" << mysin << ", std=" << stdsin << ", error=" << error << endl;
    cout << endl;
}

auto main() -> int
{

    check(0.5);
    check(30);
    check(45.4);
    check(90);
    check(151);
    check(180);
    check(195);
    check(89.5);
    check(91);
    check(270);
    check(305);
    check(360);
    return 0;
}

预期输出:

checking : 0.5
mine=0.00872653, std=0.00872654, error=2.15177e-09

checking : 30
mine=0.5, std=0.5, error=1.30766e-07

checking : 45.4
mine=0.712026, std=0.712026, error=2.07233e-07

checking : 90
mine=1, std=1, error=0

checking : 151
mine=0.48481, std=0.48481, error=2.42041e-08

checking : 180
mine=-0, std=1.22465e-16, error=1.22465e-16

checking : 195
mine=-0.258819, std=-0.258819, error=-6.76265e-08

checking : 89.5
mine=0.999962, std=0.999962, error=2.5215e-07

checking : 91
mine=0.999847, std=0.999848, error=2.76519e-07

checking : 270
mine=-1, std=-1, error=0

checking : 305
mine=-0.819152, std=-0.819152, error=-1.66545e-07

checking : 360
mine=0, std=-2.44929e-16, error=-2.44929e-16

【讨论】:

    【解决方案4】:

    我只是将这个答案保留为文档。正如 cmets 所说,我被 gcc 的许可误导了。当使用 f(42) 时,它会失败,例如作为这样的模板参数:

    std::array<int, f(42)> asdf;
    

    抱歉,这不是解决方案

    在两个不同的 constexpr 函数中分别计算浮点数和转换为 int:

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

    【讨论】:

    • reinterpret_casts,根据您的代码,不允许在常量表达式中使用。
    • 我只是尝试用 gcc 编译该代码。所以要么 gcc 比标准更宽松,要么我的解决方案还可以。
    • 前者。 GCC 不合规。 (您是否尝试执行该函数?现在,您的代码格式不正确,无需诊断)
    • GCC rejects this 如果您在需要常量表达式的上下文中使用它。
    猜你喜欢
    • 1970-01-01
    • 2014-10-06
    • 1970-01-01
    • 1970-01-01
    • 2018-02-26
    • 2010-11-05
    • 2012-09-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多