【问题标题】:Programmatically create static arrays at compile time in C++在 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 };
  1. 欢迎使用 C++0x 的解决方案
  2. 数组可能很大,很少 一百个元素长
  3. 现在的数组只包含 POD 类型
  4. 也可以假设大小为 数组将是预先知道的, 在静态编译时兼容 方式。
  5. 解决方案必须使用 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 个模板,这大大增加了编译时间。

标签: c++ metaprogramming static-array


【解决方案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

这里特别感兴趣:

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

【讨论】:

  • 查看 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 编译器实现了此功能。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2012-12-12
              • 2010-10-30
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-09-26
              • 1970-01-01
              • 2016-07-09
              相关资源
              最近更新 更多