在 C++ 中以编程方式在编译时创建静态数组

Posted

技术标签:

【中文标题】在 C++ 中以编程方式在编译时创建静态数组【英文标题】:Programmatically create static arrays at compile time in C++ 【发布时间】:2011-02-28 00:38:49 【问题描述】:

可以在编译时定义一个静态数组,如下:

const std::size_t size = 5;    
unsigned int list[size] =  1, 2, 3, 4, 5 ;

问题 1 - 是否可以通过使用各种元编程技术在编译时“以编程方式”分配这些值?

问题 2 - 假设数组中的所有值都是相同的,除了少数几个,是否可以在编译时以编程方式选择性地分配值?

例如:

const std::size_t size = 7;        
unsigned int list[size] =  0, 0, 2, 3, 0, 0, 0 ;
    欢迎使用 C++0x 的解决方案 数组可能很大,很少 一百个元素长 现在的数组只包含 POD 类型 也可以假设大小为 数组将是预先知道的, 在静态编译时兼容 方式。 解决方案必须使用 C++ (无脚本、无宏、无 pp 或基于代码生成器的解决方案)

更新: Georg Fritzsche 的解决方案非常棒,需要一些工作才能使其在 msvc 和 intel 编译器上编译,但仍然是一种非常有趣的解决问题的方法。

【问题讨论】:

@GMan:图片就像我已经解释过的那样,想知道是否可以在编译时仅使用 c++ 填充静态数组。没有隐藏的议程等。 @Hippicoder @GMan 的评论是相关的,因为你不能在 C++ 或 C++0x 中做到这一点。为读者提供上下文,专家会为您找到原始问题的(替代)合适解决方案。 假设一个进程需要一个LUT,取决于进程的模式,除了一些值之外,LUT是相同的,所有其他值都是相同的,或者可以通过评估一个简单的序列来生成比如 f(n) = 2*n 或 f(n) = 1 + n 等等... 我认为第一个可以使用递归模板并将常量 + 1 传递到每个更深层次。我现在正在调查。 @Michael Dorgan:我也想过这个问题,但似乎无法找到正确的方法,我的解决方案涉及从模板结构的枚举中获取值,但是仍然需要我实例化 n 个模板,这大大增加了编译时间。 【参考方案1】:

最接近的方法是使用 C++0x 功能从可变参数模板参数列表中初始化模板的本地或成员数组。 这当然受到最大模板实例化深度的限制,并且必须测量实际对您的情况产生显着影响的温度。

例子:

template<unsigned... args> struct ArrayHolder 
    static const unsigned data[sizeof...(args)];
;

template<unsigned... args> 
const unsigned ArrayHolder<args...>::data[sizeof...(args)] =  args... ;

template<size_t N, template<size_t> class F, unsigned... args> 
struct generate_array_impl 
    typedef typename generate_array_impl<N-1, F, F<N>::value, args...>::result result;
;

template<template<size_t> class F, unsigned... args> 
struct generate_array_impl<0, F, args...> 
    typedef ArrayHolder<F<0>::value, args...> result;
;

template<size_t N, template<size_t> class F> 
struct generate_array 
    typedef typename generate_array_impl<N-1, F>::result result;
;

用于您的1..5 案例:

template<size_t index> struct MetaFunc  
    enum  value = index + 1 ; 
;

void test() 
    const size_t count = 5;
    typedef generate_array<count, MetaFunc>::result A;

    for (size_t i=0; i<count; ++i) 
        std::cout << A::data[i] << "\n";

【讨论】:

关于模板实例化深度的说明,msvc 在 1000 左右死亡,gcc 有一个设置递归深度的选项,我已经能够根据这个建议创建一个 512 元素的 lut - 编译时间显然是比在源代码中硬编码 lut 长一点,但总的来说它工作正常!!! :D 太棒了!它本质上允许连接/扩展我在 C++03 中无法使用元模板实现的数组。我认为您应该使用 MetaFunction 参数化 ArrayHolder,以便能够定义多个具有给定数量的数组。 作为一些编译器允许的相当有限的递归深度的一种解决方法,可以在每一步向“可变值列表”添加一个以上的值,将所需的深度降低 M 倍,其中 M 是添加的值的数量。例如,对于 M=2,我们有: template class F, unsigned... args> struct generate_array_impl typedef typename generate_array_impl::value , F::value, args...>::result 结果; ;但请不要忘记处理 N%M != 0 的情况 +100 我正准备将 std::initializer_list 扔到窗外,直到我找到你的答案。在我理解这是如何工作的之前肯定需要一段时间,但我对这个从编译时到运行时的惊人桥梁感到敬畏。 TYVM。 @Xocoatzin 那是参数包扩展,参见例如here【参考方案2】:

嗯,你的要求太模糊了,很难对它们做任何事情......主要问题当然是:这些价值从何而来?

无论如何,在 C++ 中构建可以被认为是 4 个步骤:

预构建步骤:从其他格式生成头文件/源代码的脚本 预处理 模板实例化 正确编译

如果您希望排除脚本生成,那么您有两种选择:预处理和元模板编程。

据我所知,元模板编程无法做到这一点,因为据我所知,在编译时连接两个数组是不可能的。因此,我们只剩下了今天的救星:预处理器编程

我建议使用成熟的库来帮助我们:Boost.Preprocessor。

这里特别感兴趣:

BOOST_PP_FOR BOOST_PP_REPEAT

现在,如果我们知道从哪里选择值,我们就可以给出更有意义的例子。

【讨论】:

查看 Georg Fritzsche 的回答:使用 C++0x 可变参数模板和从可变参数列表初始化静态数组,他能够想出一个元模板解决方案!【参考方案3】:

从 C++17 开始,您可以使用 constexpr lambda 并就地调用它。唯一的“缺点”是您必须使用 std::array 而不是 c 样式数组:

constexpr auto myArray[]() constexpr
    std::array<MyType, MySize> result;
    for (int i = 0; i < MySize; ++i)
    
       result[i] = ...
    
    return result;
();

举个例子,你可以创建一个具有 2 次幂的数组:

constexpr auto myArray[]() constexpr
    constexpr size_t size = 64;
    std::array<long long, size> result;
    result[0] = 1;
    for (int i = 1; i < size; ++i)
    
       result[i] = result[i - 1] * 2;
    
    return result;
();

如您所见,您甚至可以引用数组的前一个单元格。

这种技术称为 IILE 或立即调用 Lambda 表达式。

【讨论】:

【参考方案4】:

如何使用模板构建嵌套结构,并将其转换为正确类型的数组。下面的示例适用于我,但我有一种感觉,我要么踩踏,要么非常接近未定义的行为。

#include <iostream>

template<int N>
struct NestedStruct

  NestedStruct<N-1> contained;
  int i;
  NestedStruct<N>() : i(N) 
;

template<>
struct NestedStruct<0> 

  int i;
  NestedStruct<0>() : i(0) 
;

int main()

  NestedStruct<10> f;
  int *array = reinterpret_cast<int*>(&f);
  for(unsigned int i=0;i<10;++i)
  
    std::cout<<array[i]<<std::endl;
  

当然,您可能会争辩说该数组不是在编译时初始化的(我认为这是不可能的),但是将进入数组的值是在编译时计算的,您可以像平常一样访问它们数组...我认为这是尽可能接近的。

【讨论】:

reinterpret_cast 在我脑海中敲响了未定义行为的警钟。 我们可以通过使用&amp;f.i-10 或添加递归int* start() 函数来避免reinterpret_cast。然而问题实际上是“编译器是否在嵌套结构中的containedi 之间插入填充?”。我看不出它为什么会这样,因为NestedStruct&lt;N&gt;int 将具有相同的对齐要求。但是,我认为规范中没有任何内容会禁止在这种情况下插入填充。 (也许比我肯定知道的更好的语言律师)。【参考方案5】:

你真的需要在编译时这样做吗?在静态初始化时这样做会容易得多。你可以这样做。

#include <cstddef>
#include <algorithm>

template<std::size_t n>
struct Sequence

    int list[n];

    Sequence()
    
        for (std::size_t m = 0; m != n; ++m)
        
            list[m] = m + 1;
        
    
;

const Sequence<5> seq1;

struct MostlyZero

    int list[5];

    MostlyZero()
    
        std::fill_n(list, 5, 0); // Not actually necessary if our only
                                 // are static as static objects are
                                 // always zero-initialized before any
                                 // other initialization
        list[2] = 2;
        list[3] = 3;
    
;

const MostlyZero mz1;

#include <iostream>
#include <ostream>

int main()

    for (std::size_t n = 0; n != 5; ++n)
    
        std::cout << seq1.list[n] << ", " << mz1.list[n] << '\n';
    

如果需要,您可以将列表推送到结构之外,但我认为这样更简洁。

【讨论】:

这些值在编译时不存在 - 我想如果我想要的很简单,我可以很容易地编写一个函数来填充 std::vector ......谢谢不过尝试一下。 @Hippicoder:如果这些值在编译时不存在,那么您将如何按照您的问题在编译时以编程方式分配它们? 我相信他是想说你的代码没有在编译时生成它们。您的代码在运行时创建数组,因此不符合他过于严格的要求...【参考方案6】:

Boost.Assignment 之类的东西可以用于标准容器。如果确实需要使用数组,可以和Boost.Array一起使用。

【讨论】:

【参考方案7】:

有时(并非总是)这样的数组是从类型数组生成的。 例如,如果您已经有可变参数类列表(如模板)并且想要存储封装的 uint32_t 值,您可以使用:

uint32_t tab[sizeof(A)]= A::value...;

【讨论】:

【参考方案8】:

第一个问题。你可以这样做。

template <int num, int cur>
struct ConsequentListInternal 
    enum value = cur;
    ConsequentListInternal<num-1,cur+1> next_elem;
;

template <int cur>
struct ConsequentListInternal<0, cur> 
    enum value = cur;
;

template <int v>
struct ConsequentList 
    ConsequentListInternal<v, 0> list;
;

int main() 
    ConsequentList<15> list;
    return 0;

【讨论】:

Ok.... 我如何从列表中获取第 i 个值,并在运行时生成 "i" ? ps:请阅读 Michael Dorgan 解决方案的评论。【参考方案9】:

只需使用代码生成器。使用表格甚至数学函数构建一个或多个可以生成所需代码的模板。然后将您生成的文件包含在您的应用中。

说真的,代码生成器会让您的生活更轻松。

【讨论】:

有两个人将此标记为垃圾邮件。它对我来说似乎不是垃圾邮件,除了您的代码生成器尚不可用,因此提及它无助于回答问题。 (一旦你的工具可用就编辑答案会有所不同。) – 我也是代码生成的忠实粉丝,这真的会让他的生活更轻松。 ;) @Roger:我已经编辑了我的答案并删除了对产品的所有引用。 现在绝对值得一票!在 SO 上自我推销是一件棘手的事情。 代码生成器可以是array_type user_impl(size_t index);,使用std::cout和逗号生成数组体。您可以使用#include 将生成的正文包含在代码中。只需像运行时初始化一样对其进行编码,然后使用主机构建的二进制文件来生成数组。对于大多数用户来说,主机、构建和目标都是相同的。【参考方案10】:

元编程可以做很多事情。 但首先我想问:你为什么要在你的情况下这样做?我可以理解您是否需要在不同的地方声明这样的数组,这样就需要多次重写相同的东西。这是你的情况吗?

通过说“以编程方式定义”,我建议如下:

#define MyArr(macro, sep) \
    macro(0) sep \
    macro(0) sep \
    macro(2) sep \
    macro(3) sep \
    macro(0) sep \
    macro(0) sep \
    macro(0)

到目前为止,我们已经以最抽象的方式定义了您想要的所有值。顺便说一句,如果这些值实际上对您有意义 - 您可以将其添加到声明中:

#define MyArr(macro, sep) \
    macro(0, Something1) sep \
    macro(0, Something2) sep \
    // ...

现在让我们为上述声明注入活力。

#define NOP
#define COMMA ,
#define Macro_Count(num, descr) 1
#define Macro_Value(num, descr) num

const std::size_t size = MyArr(Macro_Count, +); 
unsigned int list[size] =  MyArr(Macro_Value, COMMA) ;

您还可以处理大多数数组条目相同的情况,并带有一些变态的创造力:)

但你应该经常问自己:这真的值得吗?因为如您所见,您将代码变成了一个谜题。

【讨论】:

为什么要将一些应该在编译时计算的东西推回运行时?由于 C++ 语言的差距,他不得不把代码变成一个谜题。【参考方案11】:

来自提升,

boost::mpl::range_c<int,1,5>

将在编译时生成从 1 到 5 的排序数字列表。对于第二个,您没有提到将更改值的标准。我很确定一旦创建了一个列表,你就不能取消定义然后重新定义一个新的 var。

【讨论】:

带有 range_c 和其他 mpl 样式的数组是,它们没有随机访问运算符,或者如果它们有,则需要编译时索引值。我希望能够像在运行时使用静态数组一样使用该数组,并在运行时生成索引值。【参考方案12】:

使用模板递归

template<uint64_t N>
constexpr uint64_t Value()

    return N + 100;


// recursive case
template<uint64_t N, uint64_t... args>
struct Array : Array<N - 1, Value<N - 1>(), args...> 
;

// base case
template<uint64_t... args>
struct Array<0, Value<0>(), args...> 
    static std::array<uint64_t, sizeof...(args) + 1> data;
;

template<uint64_t... args>
std::array<uint64_t, sizeof...(args) + 1> Array<0, Value<0>(), args...>::data = Value<0>(), args...;

int main()

    Array<10> myArray;
    for (size_t i = 0; i < myArray.data.size(); ++i) 
        cout << myArray.data[i] << endl;
    

    return 0;

【讨论】:

【参考方案13】:

数组 t

如前所述,在 C++17 中,您可以使用 constexpr

vector<int> countBits(int num) 
    static constexpr int SIZE = 100000;
    static constexpr array<int, SIZE> t []() constexpr 
            constexpr uint32_t size = SIZE;
            array<int, size> v;
            for (int i = 0; i < size; i++)
                v[i] =  v[i>>1] + (i & 1); // or simply v[i] = __builtin_popcount(i);
            return v;();

    vector<int> v(t.begin(), t.begin() + num + 1);
    return v;

但是您必须使用 c++ 数组类型。


int t[大小]

如果你真的想使用 C 数组 int [SIZE],不同于 array&lt;int, SIZE&gt;,请使用以下技巧:

声明一个全局数组,然后在 main 中计算值以在编译时创建静态数组:

int w[100000] = 0;

vector<int> countBits(int num) 
    vector<int> v(w, w + num + 1);
    return v;


int main(void) 
    for (int i = 0; i < 100000; i++)
        w[i] = __builtin_popcount(i);



结果

运行时的输出(确实很糟糕):

OK  ( 591 cycles)        0,1,1, -> 0,1,1,
OK  ( 453 cycles)        0,1,1,2,1,2, -> 0,1,1,2,1,2,
OK  ( 455 cycles)        0,1,1,2,1,2,2,3,1,2,... -> 0,1,1,2,1,2,2,3,1,2,...

constexpr 数组的平均输出:

OK  (   1 cycles)        0,1,1, -> 0,1,1,
OK  (   2 cycles)        0,1,1,2,1,2, -> 0,1,1,2,1,2,
OK  (  24 cycles)        0,1,1,2,1,2,2,3,1,2,... -> 0,1,1,2,1,2,2,3,1,2,...

第二种方法的平均输出(略快,因为我们摆脱了 C++ 数组的开销):

OK  (   0 cycles)        0,1,1, -> 0,1,1,
OK  (   1 cycles)        0,1,1,2,1,2, -> 0,1,1,2,1,2,
OK  (  23 cycles)        0,1,1,2,1,2,2,3,1,2,... -> 0,1,1,2,1,2,2,3,1,2,...

基准测试

我的基准测试是:

#include <vector>
#include <string>
#include <cstdint>
#include <array>
#include <iostream>
#include <ctime>
#include <iterator>
#include <sstream>

using namespace std;

vector<int> nums = 2, 5;
vector<vector<int>> expected = 0,1,1, 0,1,1,2,1,2; // feel free to add more tests

for (int i = 0; i < expected.size(); i++) 
        clock_t start = clock();
        vector<int> res = countBits(nums[i]);
        double elapsedTime = (clock() - start);
        printf("%s  \033[30m(%4.0lf cycles)\033[0m\t %s -> %s\n", (expected[i] == res) ? "\033[34mOK" : "\033[31mKO", elapsedTime, toString(res).c_str(), toString(expected[i]).c_str());

【讨论】:

【参考方案14】:

随着时间的推移,constexpr 函数、方法和 lambda 的功能在 C++ 中得到了极大的改进。使用 C++17,您可以使用 for 循环和 if 条件在编译时实际计算 constexpr 数组的内容。请参阅以下示例了解素数筛:

#include <array>
#include <cmath>

template<unsigned N>
constexpr auto primesieve() 
    std::array<bool, N+1> primes ;
    // From C++20, the init loop may be written as:   primes.fill(true);
    for(unsigned n = 0; n <= N; n++) 
        primes[n] = true;
    
    unsigned maxs = sqrt(N);
    for(unsigned n = 2; n <= maxs; n++) 
        if(primes[n]) 
            for(unsigned j = n + n; j <= N; j += n) 
                primes[j] = false;
            
        
    
    return primes;
;

extern constexpr std::array<bool, 20> myprimes  primesieve<19>() ;

当您查看此代码的汇编输出时,您只会看到myprimes 数组的数据字节,而不是单个处理器指令。所有计算都在编译时执行,即使优化已关闭。

但是,正如其他人已经写的那样:在编译器中解释 C++ 代码比运行已编译的 C++ 代码要慢得多。因此,可以在编译时合理完成的那些初始化在运行时最多需要几毫秒。

但是const/constexpr初始化有很多好处。也就是说,它们进入恒定内存,在运行同一应用程序的不同进程之间共享。另一方面,运行时的动态初始化进入每个进程的私有内存。

而且能力正在进一步提高。 C++20 甚至在constexpr 函数中增加了对std::stringstd::vector 的支持。但是,您无法从 constexpr 函数中返回非空字符串和向量,并且到目前为止,只有 Microsoft 编译器实现了此功能。

【讨论】:

以上是关于在 C++ 中以编程方式在编译时创建静态数组的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在 C++ 运行时动态创建函数?

C++中为啥要动态创建对象,有啥好处

C++ 中的静态数组与动态数组

在 plpgsql 中以编程方式访问记录的列

在 EclipseLink 中以编程方式创建 <class> 定义

在 C# 中以编程方式编译打字稿?