-2

Why yet another question on string to hash conversion

My question is basically the same as the popular How to convert a std::string to const char* or char* question, but with a twist. I need the hash at compile time. Before rejecting my question, let me briefly explain my motivation.

Motivation

In my framework I am building, I have many threads. I have carefully designed the file structure so these threads re-use files whose functionality are the same so as not to violate ODR and lose maintainability. At the top of the list is error logging. My elaborate code begs to be re-used as-is in these different apps. So the initialized errorLogger object needs to be a different instance for each thread.

Proposed solution

Templatize my ErrorLogger class with a constant non-type parameter. In my framework, each app has a unique string that identifies itself. Now if I could hash that string at compile time, I would have the non-type template parameter I need for the compiler to generate separate instances.

Here is the example code that doesn't work:

#include <string>

std::string threadUniqueStr { "threadUniqueName" };
/*constexpr*/ auto threadUniqueHash = std::hash< std::string > {} ( threadUniqueStr ); // uncommented constexpr results in 'expression did not evaluate to a constant'

template< size_t >
void Func()
{}

int main()
{
  //Func< threadUniqueHash >(); // ERROR: expression did not evaluate to a constant
  Func< 33 >(); // works fine
}

But maybe there is an easier C++ way to do this that I am overlooking?

Edit 1: My Solution

Answer 1 shows how to create a hash from a string using string_view which follows @NathanOliver's advice that you have to write your own hash function for it to be constexpr. But I understand that writing your own hash function can have problems. @Pepijn Kramer points out 1) two strings may still produce the same hash and 2) from his experience, that a class hierarchy featuring app reporting at the top and individual error reporting behavior derived classes served his purposes in multi-dev situations (like mine). Since I don't want to use templates non-type parameter feature in an un-tried manner even though I can make a case for it, I am going to create my own ErrorLogger class hierarchy. Thanks to all for your helpful input.

Edit 2: My Solution 2

I ended up using my original design for my error logger. Answer 1's string_view hash lets me constexpr a unique enough hash number which I use to create explicit template specializations, one for each named project. The ErrorLogger code itself is put into a static inside the specialization. Here is what the coding structure looks like:

// .h
template< size_t ProjectNameNumT > // primary non-type template global func
void INFOMSG();
template< size_t ProjectNameNumT >
void INFOMSG( bool yesNo ); // 2nd primary template; 1st overload
// global define in Proj A
ErrorLogger< PROJ_A_HASH > errorLoggerProjA;
// global define in Proj B
ErrorLogger< PROJ_B_HASH > errorLoggerProjB;
// .cpp
template<> void INFOMSG< PROJ_A_HASH >()
{
  errorLoggerProjA.initLoggerHasRun = true; // set bool in specialization A specialization
}
// .cpp
template<> void INFOMSG< PROJ_B_HASH >()
{
  errorLoggerProjB.initLoggerHasRun = true; // set bool in specialization B specialization
}
// .cpp
template<> void INFOMSG< PROJ_B_HASH >( bool yesNo )
{
  errorLogger.initLoggerHasRun = yesNo; // uses 
}
// dev user's interface
INFOMSG< PROJ_A_HASH >(); // sets bool to true in A
INFOMSG< PROJ_B_HASH >(); // sets bool to true in B
INFOMSG< PROJ_A_HASH >( false ); // sets bool in A to whatever yesNo value which is false here

The ODR goal was achieved without sacrificing dev interface ease-of-use.

rtischer8277
  • 496
  • 6
  • 27
  • 1
    Do note that since you don't control `std::hash` you could have two different unique strings that hash to the same value. – NathanOliver Oct 31 '22 at 14:54
  • perhaps there is something slightly wrong with your design? should you want anyone else to use it. – Neil Butterworth Oct 31 '22 at 15:00
  • 4
    For the string to be usable at compile time it needs to be a constexpr string (C++20) or you can use a constexpr string_view. And then you will need to write a constexpr hash function. – Pepijn Kramer Oct 31 '22 at 15:00
  • wrt design, I would not even use a templated logger class. I would inject specific log interfaces into my functional code. That wayI can unit test my code (without concreet logging infrastructure) and check if the expected number of log calls are made from my code. The injected string? It will be part of the adapter that implements the log interface and forwards it to the logging infrastructure (which might be files today and log servers tomorrow). – Pepijn Kramer Oct 31 '22 at 15:03
  • 1
    your code that "does not work" does compile and run with without errors: https://godbolt.org/z/fnT81686Y. I know I am a nitpicker, but you better post the code that does not compile (without the need that others must edit it to reproduce the error) and the complete compiler error message. – 463035818_is_not_an_ai Oct 31 '22 at 15:05
  • for your motivation its not clear why each thread needs a seperate logger instance. At some point those loggers will have to share some resource to produce some output, and I dont see how creating a seperate instance for each thread helps with that, or how this is any better than having one single logger for many threads – 463035818_is_not_an_ai Oct 31 '22 at 15:09
  • The whole reasoning why each thread can not have its private uniquely numbered object instance of sole ErrorLogger class but instead needs to have different unique class template instantiation is quite dim, unaired and missing. – Öö Tiib Oct 31 '22 at 15:13
  • This is an example of separating logging from the rest of your code (and get unit testabillity for YourClass that) https://onlinegdb.com/o14FCLMsSx. It might not be the 'easiest' way but it is the most maintainable design I know of based on my long experience. And I think constexpr isn't needed here. Also have a look at std::format for formatting your output messages its pretty optimized. – Pepijn Kramer Oct 31 '22 at 15:17
  • @Pepijn Kramer Actually, I do have multiple ErrorLogger specializations that make it easy for the user dev to code logger statements. Not good if there is signature differentiation among thread/projects. – rtischer8277 Oct 31 '22 at 15:20
  • @rtischer8277 That's why I usually let a project/class define its own interface with report methods (instead of using one predifined log interface for ALL projects). And then someone with knowledge of the logging infrastructure writes the small adapters. (Kind of development dependency inversion). That way the functional code only has to use an interface it defined itself. (Which decouples a lot of the development chain too and delays writing the adapters and actual logging system to be used until integration time). Just food for thought :) – Pepijn Kramer Oct 31 '22 at 15:24
  • @NathanOliver Good point. This means that two downstream user devs could be using the same **errorLogger** instance. Not good. – rtischer8277 Oct 31 '22 at 15:35
  • @Pepijn Kramer "That's why I usually let a project/class define its own interface..." Would sub-classing ErrorLogger fit into what you are suggesting? That way, each downstream dev would be compiling a different class and at runtime error logger thread interaction would automatically be between different errorLogger instances. – rtischer8277 Oct 31 '22 at 15:47
  • Many functions of `std::basic_string` are `constexpr` since C++20, but they are only useable in a constexpr context, if you use a allocator useable in this context; the default one used by `std::string` is not one where this works. – fabian Oct 31 '22 at 16:25

2 Answers2

1

In C++17 a string_view can be constexpr so you can make your own hash function that takes one of those e.g. the hash function from someone's answer here would be like the following.

#include <string_view>
#include <iostream>

constexpr size_t some_hash(std::string_view sv) {
    size_t  hash = 5381;
    for (auto c : sv)
        hash = ((hash << 5) + hash) + c; 

    return hash;
}

template<size_t N>
void some_function() {
    std::cout << N << "\n";
}

int main() {
    some_function<some_hash("foobar")>();
}
jwezorek
  • 8,592
  • 1
  • 29
  • 46
0

The essence of the problem is this:

template and constexpr are required to be evaluated at compile time, std::hash is calculated at run-time and is external to the compiler.

You can say the result of the hash is const, but it seems like you want to tell the compiler to run some function. compilers usually don't do that.

SHR
  • 7,940
  • 9
  • 38
  • 57