1

I have strings which represent image names like "foobar.png" etc. As you know, switch-case in C++ does not support switching on a string.

I'm trying to work around this, by hashing the string to std::size_t, and then using that value in the switch-case statements.

For example:

        //frameName is an std::string which represents foobar.png etc..
        switch (shs(frameName)) { //shs is my hash func which returns std::size_t;
            case shs(Pfn::fs1x1): //Problem in this line
            default:{
                break;
            }
        }

In a separate file (Pfn.hpp):

namespace Pfn{ const std::string fs1x1 = "fs1x1"; };

The problem is, that in my case statement the compiler reports that shs(Pfn::fs1x1) is not a constant expression. The exact error message is:

Case value is not a constant expression:

It would be really tedious to work out all the hash-values in advance and then hardcode them into the case statements. Do you have a suggestion on how I can somehow create the constant expressions at runtime ?

EDIT: My shs function:

static std::size_t shs(std::string string){
    return Hash::shs::hs(string);
}

//...

namespace Hash{
    struct shs{
    public:
        inline std::size_t operator()(const std::string &string)const{
            return hashString(string);
        }

        static std::size_t hs(const std::string &string){
            std::size_t seed = 0;
            hash_combine(seed,string);
            return seed;
        }

        //From Boost::hash_combine.
        template <class T>
        static inline void hash_combine(std::size_t& seed, const T& v)
        {
            std::hash<T> hasher;
            seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        };
    };
}
Rahul Iyer
  • 19,924
  • 21
  • 96
  • 190

3 Answers3

3

shs's argument needs to be constexpr and shs itself must be constexpr as well. Chances are, you might want to provide different implementations for the compile-time version and the run-time version of the hash, due to C++11 constraints on constexpr functions.

I wrote a post on this very topic some time ago, using fnv1a as the hash algorithm. Here are the important parts:

constants:

typedef std::uint64_t hash_t;

constexpr hash_t prime = 0x100000001B3ull;
constexpr hash_t basis = 0xCBF29CE484222325ull;

Runtime hash:

hash_t hash(char const* str)
{
    hash_t ret{basis};

    while(*str){
        ret ^= *str;
        ret *= prime;
        str++;
    }

    return ret;
}

Compile-time hash:

constexpr hash_t hash_compile_time(char const* str, hash_t last_value = basis)
{
    return *str ? hash_compile_time(str+1, (*str ^ last_value) * prime) : last_value;
}

user defined string literal:

constexpr unsigned long long operator "" _hash(char const* p, size_t)
{
    return hash_compile_time(p);
}

and usage:

switch(fnv1a_64::hash(str)){
case "first"_hash:
    cout << "1st one" << endl;
    break;
case "second"_hash:
    cout << "2nd one" << endl;
    break;
case "third"_hash:
    cout << "3rd one" << endl;
    break;
default:
    cout << "Default..." << endl;
}

demo

But please, think of the children! Unless you can guarantee that there will be no hash collisions, this is playing with fire and is not fit to be production code.

krzaq
  • 16,240
  • 4
  • 46
  • 61
  • Hmm.. to be honest, the image assets for my app aren't ready, and it interrupts my "flow" to have to keep creating images myself to use in the app so I'm just hacking away and generating images on the fly using the switch case until the production images are ready.... Can you explain how your hash function works ? as well as your "" operator overload ? Its intriguing... I didn't know you could do that... – Rahul Iyer Nov 21 '16 at 05:12
  • I imagine if there is a finite set of strings that I am using, I could just call this function on all the strings and check for collisions, since it should be deterministic and return the same value on all platforms... where is "prime" defined... I can use any prime ? – Rahul Iyer Nov 21 '16 at 05:15
  • @John Well, as far as hash goes, I simply implemented fnv1a (linked). The user defined literal is a new addition in C++11, precisely for cases like this. As you can see it is `constexpr` itself and passes constexpr data to a function that is able to produce a hash at compile time. I'm not sure what else to say about this, but ask away. I'll update the post with primes in a second, I'm not 100% certain, but some primes are probably better than others, I used one proposed by the algorithm creators. – krzaq Nov 21 '16 at 05:18
  • @John if your input will always be one of the cases then there is no risk of collisions - and if cases themselves collide, you get a compiletime error, so that usage is safe. – krzaq Nov 21 '16 at 05:19
  • That's incredible actually... so I guess I don't even need to test whether there are collisions in the hash function! – Rahul Iyer Nov 21 '16 at 05:20
  • I still maintain that this technique should be handled with great care, but yeah, it is neat. – krzaq Nov 21 '16 at 05:22
  • The part I'm having trouble wrapping my head around is hash collisions - if it simply won't compile if there is a hash collision, then it should be safe. Also, there is a default case. I don't understand how your compile_time hash function works or how the runtime hash works... but how can this fail ? Should that be a separate stack overflow question ? – Rahul Iyer Nov 21 '16 at 05:27
  • Ah, I see where you got lost. The hash function won't fail, but from compiler's perspective case "foo"_hash: is indistinguishable from case 131313432: (assuming 131313432 is a hash of "foo"). So, if there is a hash collision, you get duplicated case expressions ⟶ compile error. – krzaq Nov 21 '16 at 05:29
  • @John But if your runtime argument is not required to be one of cases, then there is a chance - however small - that its hash will collide with one of cases and it'll enter the wrong case. I now think you might want to consider using a `std::map>` instead. This will allow runtime manipulation of keys. – krzaq Nov 21 '16 at 05:31
  • right I understand that bit - but you warned me that unless I can guarantee that there will be no hash collisions, then it is unfit to be production code... but it seems that it won't make it to production unless it compiles... and if it compiles, then it is fit for production... unless the hash-function can produce different results on different platforms, which doesn't seem to be the case... – Rahul Iyer Nov 21 '16 at 05:31
  • aah.. I'm lost again - but the runtime argument, if it isn't one of the cases will fall into the default case right ? – Rahul Iyer Nov 21 '16 at 05:32
  • @John With 99% certainty yes. But that's the problem - there are only 2^64 hashes, and there are many many orders of magnitude more of combinations of characters in strings. What I'm trying to say is, it's mathematically guaranteed that there are collisions to your hashes - it's just very unlikely. If you know your strings beforehand - you can check them all (as it happens for cases in switch) and you're fine. But imagine writing a program trying random strings until it finds a hash collision. With 64 bits it won't be fast, but sooner or later... – krzaq Nov 21 '16 at 05:35
  • I guess you could do a second comparison in each case, which wouldn't even look that bad with a macro to ensure that there's a 100% match – krzaq Nov 21 '16 at 05:39
  • well the point I'm making is (perhaps this is what I don't understand), even if there is a collision, it simply won't compile right ? So if it doesn't compile, all I have to do is change the string name.. and then it is safe for production.... Because the runtime hash and compile time hash, should be the same for the same string, therefore, it seems like nothing bad can happen (in your example), simply if it compiles... – Rahul Iyer Nov 21 '16 at 05:44
  • @John Well, as I said, it really depends on your runtime strings. If it can be unexpected then you can't know at compile time what its value will be and it *is* possible that it'll silently collide and enter the wrong case. You're only safeguarded against having collisions in your switch cases - not in the runtime strings. To put things simply, imagine that your hash function is %10 for numbers. You can easily ensure that 0-9 are the only cases used, but if - at runtime - user enter 33 or 43 - you get the same hash (3). – krzaq Nov 21 '16 at 05:47
2

I'll assume your hash won't have any collisions for purposes of this answer.

If you instead define your hash as a preprocessor function, you can create constants you can match to.

This post might help: Compile-time (preprocessor) hashing of string

Community
  • 1
  • 1
Jim B.
  • 4,512
  • 3
  • 25
  • 53
1

By definition, constant expression means an expression that can be evaluated at compile time.

But you can use case func(): if func() is constexpr.

tristan
  • 4,235
  • 2
  • 21
  • 45