使用CRC32算法在编译时散列字符串

时间:2015-02-23 14:13:10

标签: c++ c++11 visual-studio-2012 constexpr compile-time

基本上我想在我的代码中能够做到这一点:

 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 2013年11月)进行编译时得到了这个

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

如果在编译期间完成,我怎样才能使用这种方式获得WSID函数?

试过这个:Compile time string hashing

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

修改

我第一次在Jason Gregory的游戏引擎架构中听说过这种技术。我联系了那位有意回答我的作者:

  

我们所做的是通过自定义的小型预处理器传递我们的源代码,该预处理器搜索SID('xxxxxx')形式的文本,并将单引号之间的任何内容转换为其哈希等效项作为十六进制文字({{ 1}})。 [...]

     

你可以想象通过宏和/或某些模板元编程也可以做到这一点,尽管如你所说让编译器为你做这种工作很棘手。这并非不可能,但编写自定义工具更容易,也更灵活。 [...]

     

另请注意,我们为0xNNNNNNNN文字选择了单引号。这样做是为了让我们在代码编辑器中获得一些合理的语法突出显示,但如果出现问题并且某些未预处理的代码通过编译器,它会引发语法错误,因为单引号通常保留用于单字符文字。

     

另请注意,让您的小预处理工具在某种数据库中缓存字符串至关重要,这样可以在给定哈希码的情况下查找原始字符串。当您调试代码并检查SID('xxxx')变量时,调试器通常会向您显示相当难以理解的哈希代码。但是使用SID数据库,您可以编写一个插件,将这些哈希代码转换回其字符串等效项。这样,您就会在观看窗口中看到SID(' foo'),而不是StringId [...]。此外,游戏应该能够加载这个相同的数据库,以便它可以在屏幕上打印字符串而不是十六进制哈希码,以便进行调试[...]。

但是虽然预处理有一些主要优点,但这意味着我必须准备某种修改文件的输出系统(那些将存储在别处,然后我们需要告诉MSVC)。因此,它可能会使编译任务复杂化。有没有办法用python预处理文件,例如没有头痛?但这不是问题,我仍然对使用编译时功能感兴趣(关于缓存我可以使用ID索引)

2 个答案:

答案 0 :(得分:15)

这是一个完全在编译时工作的解决方案,但也可以在运行时使用。它是constexpr,模板和宏的混合体。您可能想要更改某些名称或将它们放在单独的文件中,因为它们很短。

请注意,我重用了this answer for the CRC table generation中的代码,并根据this page的代码进行了实施。

我没有在MSVC上测试它,因为我目前还没有在我的Windows VM中安装它,但我相信它应该可行,或者至少可以用来进行微不足道的更改。

以下是代码,您可以直接使用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实现,转换为与C ++ 11 constexpr一起使用的递归样式。 然后crc32WSID只是简单的包装器以方便使用。

答案 1 :(得分:1)

@ 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');

根据您的整体计划,您可能希望使用内联访问器来检索值。

如果可以接受轻微的性能影响,您也可以使用单例模式编写这样的函数。

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

当然,如果要求编译时评估的原因不同(例如,不希望某些id出现在编译代码中),这些技术将无济于事。

另一个选择是使用Jason Gregory建议的自定义预处理器。如果将所有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);

关于此的几点说明:

  • 这假设所有原始ID都可以转换为有效标识符。如果它们包含特殊字符,则预处理器可能需要进行其他重复操作。
  • 我注意到你原来的问题不匹配。你的函数返回一个int,但Engine.getById似乎是一个字符串。我建议的代码总是使用int(如果你想要总是字符串,很容易改变)。