Skip to content

Instantly share code, notes, and snippets.

@EvanMcBroom
Last active March 13, 2024 16:46
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save EvanMcBroom/2a9bed888c2755153a9616aa7ae1f79a to your computer and use it in GitHub Desktop.
Save EvanMcBroom/2a9bed888c2755153a9616aa7ae1f79a to your computer and use it in GitHub Desktop.
Switch Statements with Full Strings

Switch Statements with Full Strings

C++11 introduced the constexpr keyword for defining a constant expression. A constant expression is a variable or function that may be evaluated at compile time. This has many uses, including extending a switch statement to support full strings.

Constant Expression Hash Functions

C++ only supports using an integer as the condition in a switch statement and an integer that is known at compile time in a case statement. You can define a hash function and use it to convert a string to an integer to use in a switch statement. If you define that hash function as a constexpr you can use it to convert a string literal to an integer to use in a case statement as well.

I'll use Daniel J. Bernstein's "Times 33 with Addition" hashing algorithm (e.g. DJBX33A) as an example. It is one of the fastest and most well distributed string hashing algorithms to date. It's implementation is also tiny:

Hash(input): return *input ? *input + 33 * Hash(input + 1) : 5381;

Here is the same algorithm defined as a templated C++ constant expression function:

template <typename _T>
unsigned int constexpr Hash(_T const* input) {
    return *input ? static_cast<unsigned int>(*input) + 33 * Hash(input + 1) : 5381;
}

Full Examples

You can use the above hash function to supply a full string as the expression in a switch or case statement instead of just integers. The input to Hash in a case statement needs to be a string literal.

// Example 1: Using with a char pointer
char* userInput{ GetUserInput() };
switch (Hash(userInput) {
    case Hash("match 1"): RunCode(); break;
    case Hash("match 2"): RunMoreCode(); break;
    default: break;
}
free(userInput);

// Example 2: Using with an std::string
std::string userInput{ GetUserInput() };
switch (Hash(userInput.data())) {
    case Hash("match 1"): RunCode(); break;
    case Hash("match 2"): RunMoreCode(); break;
    default: break;
}

You can also use the above examples with a wchar_t pointer or std::wstring as well by using a wide character string literal in the case statement (e.g. Hash(L"match 1")).

Hash's Behavior

A constant expression will be evaluated by the compiler if the result of the expression needs to be known at compile time. By providing a string literal to Hash in the case statement, the compiler will evaluate the expression and only store the resultant integer in the compiled code. The input to Hash in the switch statement will not be known at compile time and the switch statement will be evaluated at runtime.

Implications

A constant expression hash function provides an easy way to use full strings in a switch statement that is both fast and small. Hashing strings at compile time prevents storing potentially sensitive strings in the .data or .rdata sections of an executable. This can also be used in position indepenedent code or shellcode because the result of Hash in the case statement will be stored directly in the .text section of the executable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment