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

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

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 + + < strong > < em > (没有脚本,没有宏,没有 pp) 或基于代码生成器的解决方案)

更新: Georg Fritzsche 的解决方案是惊人的,需要一些工作才能在 msvc 和 Intel 编译器上进行编译,但仍然是一个非常有趣的解决问题的方法。

40279 次浏览

你真的需要在编译器的时候做吗?在静态初始化时更容易做到这一点。你可以这么做。

#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';
}
}

如果愿意,您可以将列表推到 struct 之外,但是我认为这样会更干净一些。

使用元编程可以做很多事情。 但是首先我想问: 为什么你想要在你的情况下这样做?如果您需要在不同的地方声明这样的数组,那么我可以理解,因为它需要多次重写相同的内容。这是你的案子吗?

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

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

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

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

助推器,任务这样的东西可以用于标准容器。如果确实需要使用数组,可以沿着 推进,数组使用它。

来自助推器,

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

将在编译时生成从1到5的已排序数字列表。对于第二种情况,您没有提到要更改哪些值的标准。我敢肯定,一旦创建了一个列表,您就不能对一个新的 var 进行 undef 然后 redef。

1’t 问题。你可以这样做。

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

如何使用模板构建嵌套结构,并将其转换为正确类型的数组。下面的例子对我很有用,但是我有一种感觉,我要么正在涉足,要么正在接近未定义的行为。

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

当然,你可以争辩说数组不是在编译时初始化的(我认为这是不可能的) ,但是进入数组的值是在编译时计算的,你可以像访问普通数组一样访问它们... ... 我认为这是你能得到的最接近的结果。

好吧,你的要求是如此的模糊,以至于很难对它们做任何事情... ... 主要的问题当然是: 这些价值从何而来?

无论如何,C + + 中的构建可以被看作是4个步骤:

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

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

据我所知,元模板编程在这里没有办法做到这一点,因为据我所知,在编译时不可能连接两个数组。因此,我们只剩下今天的救世主: 预处理程序设计

我建议使用一个成熟的图书馆来帮助我们: 启动,预处理器

这里特别有趣的是:

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

最接近的方法是使用 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";
}

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

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

有时(并不总是)这样的数组是由类型的数组生成的。 例如,如果您已经有可变的类列表(如模板) ,并希望存储封装的 uint32 _ t 值,您可以使用:

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

使用模板递归

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

因为 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 表达式。

Array < int,SIZE > t

如前所述,对于 C + + 17,您可以使用 conexpr

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 [ SIZE ]

如果你真的想使用一个 C 数组 int [SIZE],不同于 array<int, SIZE>使用以下技巧:

声明一个全局数组,然后在 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());
}

随着时间的推移,在 C + + 中,constexpr函数、方法和 lambdas 的性能有了很大的提高。使用 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 编译器实现了这个特性。