Использование CRC32-алгоритма для хэш-строки во время компиляции

В основном я хочу, чтобы в моем коде это было возможно:

 Engine.getById(WSID('some-id'));

Который должен быть преобразован

 Engine.getById('1a61bc96');

перед компиляцией в asm. Итак во время компиляции.

Это моя попытка

constexpr int WSID(const char* str) {
    boost::crc_32_type result;
    result.process_bytes(str,sizeof(str));
    return result.checksum();
}

Но я получаю это при попытке скомпилировать с MSVC 18 (CTP November 2013)

error C3249: illegal statement or sub-expression for 'constexpr' function

Как я могу получить функцию WSID, используя этот способ или любой, если это выполняется во время компиляции?

Пробовал это: Компилировать хэширование строки времени

 warning C4592: 'crc32': 'constexpr' call evaluation failed; function will be called at run-time

EDIT:

Я впервые услышал об этой технике в Game Engine Architecture Джейсоном Грегори. Я связался с автором, который позаботился об этом мне:

Мы должны передать наш исходный код через пользовательский небольшой предварительный процессор, который ищет текст формы SID('xxxxxx') и преобразует все, что находится между одинарными кавычками в его хэшированный эквивалент как шестнадцатеричный литерал (0xNNNNNNNN), [...]

Возможно, вы могли бы сделать это с помощью макроса и/или метапрограммирования шаблонов тоже, хотя, как вы говорите, сложно сделать компилятор для такого рода работы для вас. Это не невозможно, но написать собственный инструмент проще и гораздо более гибким. [...]

Заметим также, что мы выбрали одиночные кавычки для SID('xxxx') литералов. Это было сделано для того, чтобы мы получили разумную подсветку синтаксиса в наших редакторах кода, но если что-то пошло не так, и какой-то непереработанный код когда-либо доводил его до компилятора, он бы выбросил синтаксическую ошибку, поскольку одинарные кавычки обычно зарезервированы для односимвольные литералы.

Обратите также внимание на то, что важно, чтобы ваш небольшой инструмент предварительной обработки кэшировал строки в какой-либо базе данных, так что исходные строки можно искать с учетом хэш-кода. Когда вы отлаживаете свой код и вы проверяете переменную StringId, отладчик обычно показывает вам довольно непонятный хеш-код. Но с базой данных SID вы можете написать плагин, который преобразует эти хэш-коды обратно в их эквиваленты строк. Таким образом, вы увидите SID ('foo') в окне просмотра, а не 0x75AE3080 [...]. Кроме того, игра должна иметь возможность загружать эту же базу данных, чтобы она могла печатать строки вместо шестнадцатеричных хеш-кодов на экране для целей отладки [...].

Но в то время как препроцесс имеет некоторые основные преимущества, это означает, что я должен подготовить какую-то систему вывода модифицированных файлов (они будут храниться в другом месте, а затем мы должны сообщить MSVC). Таким образом, это может усложнить задачу компиляции. Есть ли способ препроцессить файл с помощью python, например, без головных болей? Но это не вопрос, и меня все еще интересует использование функции времени компиляции (о кеше я мог бы использовать индекс ID)

Ответ 1

Вот решение, которое полностью работает во время компиляции, но может также использоваться во время выполнения. Это сочетание constexpr, шаблонов и макросов. Вы можете захотеть изменить некоторые имена или поместить их в отдельный файл, так как они довольно короткие.

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

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

Вот код, вы можете напрямую использовать функцию crc32 или функцию WSID, которая более точно соответствует вашему вопросу:

#include <cstring>
#include <cstdint>
#include <iostream>

// Generate CRC lookup table
template <unsigned c, int k = 8>
struct f : f<((c & 1) ? 0xedb88320 : 0) ^ (c >> 1), k - 1> {};
template <unsigned c> struct f<c, 0>{enum {value = c};};

#define A(x) B(x) B(x + 128)
#define B(x) C(x) C(x +  64)
#define C(x) D(x) D(x +  32)
#define D(x) E(x) E(x +  16)
#define E(x) F(x) F(x +   8)
#define F(x) G(x) G(x +   4)
#define G(x) H(x) H(x +   2)
#define H(x) I(x) I(x +   1)
#define I(x) f<x>::value ,

constexpr unsigned crc_table[] = { A(0) };

// Constexpr implementation and helpers
constexpr uint32_t crc32_impl(const uint8_t* p, size_t len, uint32_t crc) {
    return len ?
            crc32_impl(p+1,len-1,(crc>>8)^crc_table[(crc&0xFF)^*p])
            : crc;
}

constexpr uint32_t crc32(const uint8_t* data, size_t length) {
    return ~crc32_impl(data, length, ~0);
}

constexpr size_t strlen_c(const char* str) {
    return *str ? 1+strlen_c(str+1) : 0;
}

constexpr int WSID(const char* str) {
    return crc32((uint8_t*)str, strlen_c(str));
}

// Example usage
using namespace std;

int main() {
    cout << "The CRC32 is: " << hex << WSID("some-id") << endl;
}

Первая часть заботится о создании таблицы констант, а crc32_impl - это стандартная реализация CRC32, преобразованная в рекурсивный стиль, который работает с С++ 11 constexpr. Тогда crc32 и WSID просто удобны для удобства.

Ответ 2

@tux3 ответ довольно гладкий! Трудно поддерживать, тем не менее, потому что вы в основном пишете собственную реализацию CRC32 в командах препроцессора.

Еще один способ решить ваш вопрос - вернуться и понять потребность в этом требовании. Если я понимаю вас правильно, беспокойство, похоже, является производительностью. В этом случае есть вторая точка времени, которую вы можете назвать своей функцией без влияния на производительность: при загрузке программы. В этом случае вы получите доступ к глобальной переменной вместо передачи константы. По производительности, после инициализации оба должны быть одинаковыми (const выберет 32 бита из вашего кода, глобальная переменная извлекает 32 бита из регулярного расположения памяти).

Вы можете сделать что-то вроде этого:

static int myWSID = 0;

// don't call this directly
static int WSID(const char* str) {
  boost::crc_32_type result;
  result.process_bytes(str,sizeof(str));
  return result.checksum();
}

// Put this early into your program into the
// initialization code.
...
myWSID = WSID('some-id');

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

Если незначительное влияние на производительность приемлемо, вы также должны написать свою функцию следующим образом, в основном используя шаблон singleton.

// don't call this directly
int WSID(const char* str) {
  boost::crc_32_type result;
  result.process_bytes(str,sizeof(str));
  return result.checksum();
}

// call this instead. Note the hard-coded ID string.
// Create one such function for each ID you need to
// have available.
static int myWSID() {
   // Note: not thread safe!
   static int computedId = 0;
   if (computedId == 0)
      computedId = WSID('some-id');
   return computedId;
}

Конечно, если причина для запроса оценки времени компиляции является чем-то другим (например, не желая, чтобы какой-то идентификатор появлялся в скомпилированном коде), эти методы не помогут.

Другой вариант - использовать предложение Джейсона Грегори о пользовательском препроцессоре. Это можно сделать достаточно чисто, если вы соберете все IDS в отдельный файл. Для этого файла не требуется синтаксис C. Я бы дал ему расширение, такое как .wsid. Пользовательский препроцессор генерирует из него H файл.

Вот как это могло выглядеть:

idcollection.wsid(перед пользовательским препроцессором):

some_id1
some_id2
some_id3

Ваш препроцессор будет генерировать следующий файл idcollection.h:

#define WSID_some_id1 0xabcdef12
#define WSID_some_id2 0xbcdef123
#define WSID_some_id3 0xcdef1234

И в вашем коде вы бы назвали

Engine.getById(WSID_some_id1);

Несколько замечаний об этом:

  • Предполагается, что все исходные идентификаторы могут быть преобразованы в действительные идентификаторы. Если они содержат специальные символы, вашему препроцессору, возможно, потребуется выполнить дополнительные настройки.
  • Я заметил несоответствие в вашем исходном вопросе. Ваша функция возвращает int, но Engine.getById, похоже, принимает строку. Мой предлагаемый код всегда будет использовать int (легко изменить, если вы хотите всегда строку).

Ответ 3

Если кому-то интересно, я закодировал функцию генератора таблицы CRC-32 и функцию генератора кода, используя функции constexpr в стиле С++ 14. Результат, на мой взгляд, гораздо более удобный код, чем многие другие попытки, которые я видел в Интернете, и он находится далеко, далеко от препроцессора.

Теперь он использует пользовательский std:: array 'clone', называемый cexp:: array, потому что g++, похоже, не добавил ключевое слово constexpr к своему оператору доступа/записи без константы.

Однако он довольно легкий, и, надеюсь, ключевое слово будет добавлено в std:: array в ближайшем будущем. Но сейчас очень простая реализация массива выглядит следующим образом:

namespace cexp
{

    // Small implementation of std::array, needed until constexpr
    // is added to the function 'reference operator[](size_type)'
    template <typename T, std::size_t N>
    struct array {
        T m_data[N];

        using value_type = T;
        using reference = value_type &;
        using const_reference = const value_type &;
        using size_type = std::size_t;

        // This is NOT constexpr in std::array until C++17
        constexpr reference operator[](size_type i) noexcept {
            return m_data[i];
        }

        constexpr const_reference operator[](size_type i) const noexcept {
            return m_data[i];
        }

        constexpr size_type size() const noexcept {
            return N;
        }
    };

}

Теперь нам нужно создать таблицу CRC-32. Я основал алгоритм от некоторого кода Hacker Delight, и он, вероятно, может быть расширен для поддержки многих других алгоритмов CRC. Но, увы, мне нужна только стандартная реализация, так вот:

// Generates CRC-32 table, algorithm based from this link:
// http://www.hackersdelight.org/hdcodetxt/crc.c.txt
constexpr auto gen_crc32_table() {
    constexpr auto num_bytes = 256;
    constexpr auto num_iterations = 8;
    constexpr auto polynomial = 0xEDB88320;

    auto crc32_table = cexp::array<uint32_t, num_bytes>{};

    for (auto byte = 0u; byte < num_bytes; ++byte) {
        auto crc = byte;

        for (auto i = 0; i < num_iterations; ++i) {
            auto mask = -(crc & 1);
            crc = (crc >> 1) ^ (polynomial & mask);
        }

        crc32_table[byte] = crc;
    }

    return crc32_table;
}

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

// Stores CRC-32 table and softly validates it.
static constexpr auto crc32_table = gen_crc32_table();
static_assert(
    crc32_table.size() == 256 &&
    crc32_table[1] == 0x77073096 &&
    crc32_table[255] == 0x2D02EF8D,
    "gen_crc32_table generated unexpected result."
);

Теперь, когда таблица сгенерирована, нужно время генерировать коды CRC-32. Я снова основал алгоритм от ссылки Hacker Delight, и на данный момент он поддерживает только входные данные из c-строки.

// Generates CRC-32 code from null-terminated, c-string,
// algorithm based from this link:
// http://www.hackersdelight.org/hdcodetxt/crc.c.txt 
constexpr auto crc32(const char *in) {
    auto crc = 0xFFFFFFFFu;

    for (auto i = 0u; auto c = in[i]; ++i) {
        crc = crc32_table[(crc ^ c) & 0xFF] ^ (crc >> 8);
    }

    return ~crc;
}

Для завершения я генерирую один код CRC-32 ниже и статически проверяю, имеет ли он ожидаемый результат, а затем распечатает его в выходной поток.

int main() {
    constexpr auto crc_code = crc32("some-id");
    static_assert(crc_code == 0x1A61BC96, "crc32 generated unexpected result.");

    std::cout << std::hex << crc_code << std::endl;
}

Надеюсь, это поможет кому-то еще, кто хочет добиться компиляции времени CRC-32 или даже вообще.