转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=3361

多年前我写过编译期字符串Hash再探编译期字符串Hash两篇博文,分别证明了C++98下无法实现编译期的字符串hash,以及如何在C++11下用constexpr实现。过了这么多年,原有的实现在Clang上出现了严重的编译性能下降,需要一些修改才能顺利编译。而vc14也开始支持constexpr了,经过实验,发现问题仍很严重。所以这里不得不再次试着改进编译期字符串hash的方法。

旧方法回顾

上次的实现用了constexpr配合模板嵌套,实现了一个初步的编译期计算字符串hash的方法。

constexpr size_t _Hash(const char (&str)[1])
{
   return *str + 0x9e3779b9;
}

template <size_t N>
constexpr size_t _Hash(const char (&str)[N])
{
   typedef const char (&truncated_str)[N - 1];
   #define seed _Hash((truncated_str)str)
   return seed ^ (*(str + N - 1) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
   #undef seed
}

template <size_t N>
constexpr size_t CTHash(const char (&str)[N])
{
   typedef const char (&truncated_str)[N - 1];
   return _Hash<N - 1>((truncated_str)str);
}

这个方法在MinGW上可以通过,编译和执行都没问题。但在MacOSX上编译几乎要消耗无穷多的内存,因为那个宏会造成3^N次展开。而Clang 3.4和g++ 5其实是支持Relaxing requirements on constexpr functions这个C++14的特性,对constexpr的函数要求较宽松。这里把seed的宏改成size_t seed = _Hash((truncated_str)str);也是可以的。这么做能暂时解决Clang和g++的编译性能。

但是,在vc14上(目前的RC版本),遇到这种状况就会忽略constexpr,而当作一个执行期函数来处理。编译挺快,执行起来慢的不行。如果改回宏,编译也会遇到和Clang/g++一样的情况。所以这个旧方法不适用于现在的情况。

新方法

目前的需求是,在C++11的范畴内,基于原有的探索,做一个真正能解决编译期字符串hash的方法。既然C++11 constexpr的函数只能有个return语句,那就别模板展开了,全都放一行。

constexpr size_t _Hash(char const * str, size_t seed)
{
   return 0 == *str ? seed : _Hash(str + 1, seed ^ (*str + 0x9e3779b9 + (seed << 6) + (seed >> 2)));
}

#define CT_HASH(x) (_Hash(x, 0))

嗯,这么简单的一个函数,就(几乎)解决了问题。在vc14/g++ 4.6/clang 3.4上,全都能用。

可是为什么说几乎呢?在vc14上,我发现一个实现上的区别。vc14编译器只会在不得已的情况下,才会把一个constexpr的表达式变成编译器常量,平常还是把它放到了运行期。所以直接这么用占不到什么便宜。那好吧,既然如此,就让它不得已。

template <size_t N>
struct EnsureConst
{
   static const size_t value = N;
};

#define CT_HASH(x) (EnsureConst<_Hash(x, 0)>::value)

有了这个模板,_Hash(x, 0)就必须是编译期常量。在vc14上,这么做顺利达成了目标。

就这么解决了?很遗憾的是,在g++和clang上,仍需要用#define CT_HASH(x) (_Hash(x, 0))。否则会出现一个链接错误。目前我用了个#ifdef把这两种情况分开,保留以后进一步简化的可能性。

总结

编译期字符串常量的问题可以认为彻底解决了。新的方法可以保证它是一个真正的编译期常量,不是运行期,也不是优化期。经过这个改进,字符串常量的比较比以前快的多了,代码也简单得多。希望对有类似需求的朋友有所帮助。