Программно создавать статические массивы во время компиляции в 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++ (без сценариев, макросов, решений на основе pp или генератора кода, пожалуйста)

ОБНОВЛЕНИЕ: Решение Георгия Фрицше удивительно, требует небольшой работы, чтобы компилировать его на компиляторах msvc и intel, но, тем не менее, очень интересный подход к проблеме.

14 ответов

Решение

Самое близкое, что вы можете получить, - это использовать функции 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";
}

Начиная с C++17 вы можете использовать constexprлямбда вызывает его на месте. Единственный "недостаток" в том, что вам придется использоватьstd::array вместо массива в стиле c:

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

В качестве примера, как вы можете создать массив со степенью двойки:

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

Как видите, вы даже можете ссылаться на предыдущие ячейки массива.

Ну, ваши требования настолько расплывчаты, что с ними трудно что-либо сделать... Главный вопрос, конечно: откуда эти ценности?

В любом случае сборку в C++ можно рассматривать как 4 шага:

  • Предварительная сборка: создание скрипта заголовка / источника из других форматов
  • предварительная обработка
  • Шаблонные экземпляры
  • Собственная сборка

Если вы хотите исключить генерацию скриптов, то у вас есть 2 варианта: предварительная обработка и программирование мета-шаблонов.

Я просто не знаю, как программирование мета-шаблонов могло бы сработать здесь, потому что, насколько я знаю, невозможно объединить два массива во время компиляции. Таким образом, мы остаемся с спасителем дня: Программирование препроцессора

Я бы предложил использовать полноценную библиотеку, чтобы помочь нам: Boost.Preprocessor.

Особый интерес здесь:

Теперь, если бы мы только знали, где выбирать значения, мы могли бы привести более содержательные примеры.

Как насчет создания вложенной структуры с использованием шаблонов и преобразования ее в массив нужного типа. Приведенный ниже пример работает для меня, но у меня есть ощущение, что я либо ступаю, либо иду очень близко к неопределенному поведению.

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

И, конечно, вы можете утверждать, что массив не инициализируется во время компиляции (что, я думаю, невозможно), но значения, которые попадут в массив, вычисляются во время компиляции, и вы можете обращаться к ним, как если бы это был обычный массив. Я думаю, что это так близко, как вы можете получить.

Иногда (не всегда) такой массив генерируется из массива типов. Например, если у вас уже есть список классов переменных (например, шаблон) и вы хотите сохранить инкапсулированное значение uint32_t, вы можете использовать:

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

Просто используйте генератор кода. Создайте один или несколько шаблонов, которые могут генерировать нужный код, используя таблицу или даже математические функции. Затем включите файл, который вы создали в вашем приложении.

Серьезно, генератор кода сделает вашу жизнь намного проще.

Что-то вроде Boost.Assignment может работать для стандартных контейнеров. Если вам действительно нужно использовать массивы, вы можете использовать их вместе с Boost.Array.

Вы действительно должны сделать это во время компиляции? Это было бы намного проще сделать во время статической инициализации. Вы могли бы сделать что-то вроде этого.

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

Вы можете выдвинуть списки за пределы структур, если хотите, но я подумал, что это немного чище, как это.

Со временем возможности функций, методов и лямбда-выражений в C++ значительно улучшились. В C++ 17 вы можете использовать циклы for и условия if для фактического вычисления содержимого массива во время компиляции. См. Этот пример для сита простых чисел:

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

Когда вы посмотрите на вывод сборки этого кода, вы увидите только байты данных массив, а не отдельная инструкция процессора. Все вычисления выполняются во время компиляции, даже если оптимизация отключена.

Однако, как уже писали другие: интерпретация кода C++ в компиляторе намного медленнее, чем выполнение скомпилированного кода C++. Таким образом, эти инициализации, которые можно разумно выполнить во время компиляции, потребуют не более нескольких миллисекунд во время выполнения.

Но / инициализация имеет много преимуществ. А именно, они попадают в постоянную память, которая используется разными процессами, выполняющими одно и то же приложение. С другой стороны, динамическая инициализация во время выполнения переходит в частную память каждого процесса.

И возможности продолжают улучшаться. C++ 20 даже добавляет поддержку а также в функциях. Однако вы не можете возвращать непустые строки и векторы из функций, и до сих пор только компилятор Microsoft реализовал эту функцию.

1 вопрос Вы можете сделать это так.

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

массив <int, SIZE> 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<int, SIZE> используйте следующий трюк:

Объявите глобальный массив, а затем вычислите значения внутри основного для создания статического массива во время компиляции:

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

От повышения,

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

Создает список отсортированных чисел от 1 до 5 во время компиляции. Во-вторых, вы не упоминаете никаких критериев, по которым значения будут изменены. Я почти уверен, что вы не можете отменить определение, а затем переопределить новую переменную после создания списка.

Использовать рекурсивный шаблон

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

Есть много вещей, которые вы можете сделать с метапрограммированием. Но сначала я хотел бы спросить: почему вы хотите сделать это в вашем случае? Я мог бы понять, нужно ли вам объявлять такой массив в разных местах, чтобы он требовал переписывать одни и те же вещи несколько раз. Это твой случай?

Говоря "определите программно", я предлагаю следующее:

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

Вы также можете справиться с ситуацией, когда большинство ваших записей массива одинаковы, с некоторым извращенным творчеством:)

Но вы всегда должны спросить себя: стоит ли это того? Потому что, как вы видите, вы превращаете код в головоломку.

Другие вопросы по тегам