7

I thought I'd try selecting different options as strings by hashing them, but this doesn't work:

#include <type_traits>
#include <string>

inline void selectMenuOptionString(const std::string& str)
{
    switch (std::hash<std::string>()(str))
    {
    case std::hash<std::string>()(std::string("Selection one")) : break; 
        // Expression must have a constant value
    }
}

inline void selectMenuOptionString2(const std::string& str)
{
    size_t selectionOneHash = std::hash<std::string>()(std::string("Selection one"));

    switch (std::hash<std::string>()(str))
    {
    case selectionOneHash: // Expression must have a constant value 
                           // The variable of selectionOneHash cannot be used as a constant
    }

    constexpr size_t hash = std::hash<int>()(6); // Expression must have a constant value
}

It seems I can't get hash values at compile time. From what I've read each different input should yield the same unique output every time, with a very low chance of collision. Given these properties couldn't the hash value be calculated at compile time? I don't know much at all about hashing, I usually use an unordered_map, but I wanted to try something new for learning's sake.

Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • `std::hash` is evaluated at runtime. It cannot be used for compile-time hashing. – Remy Lebeau Feb 20 '18 at 23:59
  • *A* hash value could in principle be computed at compile time, but `std::hash` from the standard library is not currently specified as `constexpr`, so it cannot. Moreover, you cannot currently create a `std::string` constant expression (because there is no constexpr `operator new`, yet). The best shot facing the future is perhaps to consider `std::hash`, but we don't quite have that constexpr yet, either. – Kerrek SB Feb 21 '18 at 00:04
  • 3
    You could of course just copy the implementation of `std::hash::operator()` and paste it into a constexpr function. – Kerrek SB Feb 21 '18 at 00:05
  • 1
    Related: [Compile time string hashing](https://stackoverflow.com/q/2111667/1896169), but that's for C++11; with C++14 or C++17, we can do better. – Justin Feb 21 '18 at 00:09
  • @Kerrek This is interesting, I've been looking at it, it's basically two lines in a loop repeated for the array: _Val ^= (size_t)_First[_Next]; and _Val *= _FNV_prime; – Zebrafish Feb 21 '18 at 00:12
  • @KerrekSB And why constexpr `std::string` require constexpr `operator new`? I don't see the connection – Severin Pappadeux Feb 21 '18 at 00:23
  • @SeverinPappadeux: I was oversimplifying. `std::string` would need a constexpr `std::allocator::allocate` (which it calls to obtain storage to store the string). – Kerrek SB Feb 21 '18 at 00:24
  • @ShadowRanger well, in many cases `std::string` stores the underlying data in the pointer itself (so-called small buffer optimization), no dynamically allocated memory required – Severin Pappadeux Feb 21 '18 at 00:27
  • @KerrekSB even if strings are implemented with small buffer optimization and wouldn't be required memory allocation? – Severin Pappadeux Feb 21 '18 at 00:28
  • @SeverinPappadeux: Could you provide a small demo implementation? – Kerrek SB Feb 21 '18 at 00:33
  • @KerrekSB https://akrzemi1.wordpress.com/2014/04/14/common-optimizations/ - this is general idea, and I believe GCC since v6 has it – Severin Pappadeux Feb 21 '18 at 01:15
  • @SeverinPappadeux: I mean an example that's usable as a constant expression! (I know how SBO works. I wanted you to elaborate how you want to leverage it to get a constexpr string.) You can start by implementing the (trivial!) destructor. – Kerrek SB Feb 21 '18 at 01:24

4 Answers4

11

std::hash::operator() isn't constexpr, so you can't just use it. Instead, you'd have to write your own constexpr hash function. For example, the following is the FNV-1a hash algorithm (untested):

template <typename Str>
constexpr size_t hashString(const Str& toHash)
{
    // For this example, I'm requiring size_t to be 64-bit, but you could
    // easily change the offset and prime used to the appropriate ones
    // based on sizeof(size_t).
    static_assert(sizeof(size_t) == 8);
    // FNV-1a 64 bit algorithm
    size_t result = 0xcbf29ce484222325; // FNV offset basis

    for (char c : toHash) {
        result ^= c;
        result *= 1099511628211; // FNV prime
    }

    return result;
}

And then you can use it:

int selectMenuOptionString(const std::string& str)
{
    switch (hashString(str))
    {
    case hashString(std::string_view("Selection one")): return 42; 
    default: return 0;
    }
}

Note that if you wrote hashString("Selection one"), it would actually hash the null terminator as well, so you might want to have an overload to catch string literals, such as:

template <size_t N>
constexpr size_t hashString(char const (&toHash)[N])
{
    return hashString(std::string_view(toHash));
}

Demo

Justin
  • 24,288
  • 12
  • 92
  • 142
  • Nice - I'm a bit behind the times particularly on `constexpr`. – Lightness Races in Orbit Feb 21 '18 at 00:32
  • In the hash function seeing as it iterates all the way to the last char, and it does an XOR with the size_t, I thought it would overrun the buffer, but instead XORs only the 8 bits, it demotes it, so to speak. Then does that make result only hold the least significant eight bits and the rest is empty? – Zebrafish Feb 21 '18 at 00:41
  • @Zebrafish You could just [try it](https://wandbox.org/permlink/JDBOqokIt6VjCzE8). Clearly there are more bits set. But also, the [promotion rules](https://stackoverflow.com/a/23873536/1896169) means that the `char` would be promoted to a `size_t` for the XOR. – Justin Feb 21 '18 at 00:46
3

You'll need to implement your own hash function, because there's no suitable instantiation of std::hash that's constexpr. Here's a cheap-and-dirty...

EDIT: In order not to be humiliated too badly by Justin's answer, I added a 32 bit branch.

    constexpr size_t hash(const char *str) {
    static_assert(sizeof(size_t) == 8 || sizeof(size_t) == 4);
    size_t h = 0;

    if constexpr(sizeof(size_t) == 8) {
        h = 1125899906842597L; // prime
    } else {
        h = 4294967291L;
    }

    int i = 0;
    while (str[i] != 0) {
        h = 31 * h + str[i++];
    }

    return h;
}
AustinWBryan
  • 3,249
  • 3
  • 24
  • 42
Jive Dadson
  • 16,680
  • 9
  • 52
  • 65
  • I'm new to hash functions, but damn, it seems like a bunch of random steps. Haha. I have noticed the prime number as a common theme though. – Zebrafish Feb 21 '18 at 00:44
0

I just wanted to add this because I think it's cool. The constexpr strlen I got from a question here: constexpr strlen

#include <iostream>
#include <string>

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

size_t constexpr Hash(const char *first)
{   // FNV-1a hash function 
    const size_t FNVoffsetBasis = 14695981039346656037ULL;
    const size_t FNVprime = 1099511628211ULL;
    const size_t count = strlength(first);
    size_t val = FNVoffsetBasis;
    for (size_t next = 0; next < count; ++next)
    {
        val ^= (size_t)first[next];
        val *= FNVprime;
    }
    return val;
}

inline void selectMenuOptionString(const std::string& str)
{
    switch (Hash(str.c_str()))
    {
    case Hash("Selection one"): /*Do something*/ break;
    case Hash("Selection two"): /*Do something*/ break;
    }
}

int main()
{
    static_assert(strlength("Hello") == 5, "String length not equal");
}
Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • Post C++14, you can write the `constexpr` strlen much more simply; you don't have to use recursion. Post C++17, you could also just write `std::char_traits::length(str)` as your `constexpr` strlen – Justin Feb 21 '18 at 18:05
  • You can also do a `constexpr` strlen that doesn't do any work for string literals, using templates: `template constexpr size_t strlength(char const (&)[N]) { return N - 1; }` (the `- 1` is to ignore the null terminator) – Justin Feb 21 '18 at 18:06
  • @Justin That last solution is totally awesome, but I don't understand it. What is that argument exactly? A reference to const char array right? Without the reference it would decay to a pointer and you wouldn't be able to get the array size, right? Why the brackets around the &? Is it because without the brackets it's interpreted as an array or references? This syntax business is really hard. – Zebrafish Feb 21 '18 at 18:34
  • That is all exactly right. It's ugly stuff, so I always just go to [`std::end` #2](http://en.cppreference.com/w/cpp/iterator/end) and see what the syntax is when I forget it – Justin Feb 21 '18 at 18:35
  • @Justin Why won't it accept strlength(const char arg[N])? I pass in strlength("hel"); and the compiler says: strlength(const char [N])': could not deduce template argument for 'const char [N]' from 'const char [4]'. So it knows it's a char array of [4], not a pointer. – Zebrafish Feb 21 '18 at 18:38
  • TBH, I can't remember. I just know that for C-style arrays, if you want to compiler to deduce the length, the array has to be passed by reference. – Justin Feb 21 '18 at 18:39
-1

You can't get the hash of a runtime value at compile-time, no.

Even if you passed std::hash a constant expression, it is not defined to be able to do its hashing work at compile-time.

As far as I know (which isn't far), you'd have to come up with some monstrous template metahackery (or, worse, macros!) to do this. Personally, if your text input is known at build, I'd just pregenerate a hash outside of the code, perhaps in some Python-driven pre-build step.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • I like Kerrek's idea of copying the implementation into a constexpr function. I looked it up, it's in the public domain and it's basically two lines, very nice. Microsoft has basically used the exact implementation with the exact numbers from the Wikipedia article for the FNV hash function. – Zebrafish Feb 21 '18 at 00:21
  • Since C++14, you don't need any monstrous template metahackery; you can just use `constexpr` functions (well, you could in C++11 as well, but you'd have to use recursion, which makes it much more difficult to understand) – Justin Feb 21 '18 at 00:30