【问题标题】:Hashing types at compile-time in C++17/C++2a在 C++17/C++2a 的编译时散列类型
【发布时间】:2019-10-11 00:35:38
【问题描述】:

考虑以下代码:

#include <iostream>
#include <type_traits>

template <class T>
constexpr std::size_t type_hash(T) noexcept 
{
    // Compute a hash for the type
    // DO SOMETHING SMART HERE
}

int main(int argc, char* argv[])
{
    auto x = []{};
    auto y = []{};
    auto z = x;
    std::cout << std::is_same_v<decltype(x), decltype(y)> << std::endl; // 0
    std::cout << std::is_same_v<decltype(x), decltype(z)> << std::endl; // 1
    constexpr std::size_t xhash = type_hash(x);
    constexpr std::size_t yhash = type_hash(y);
    constexpr std::size_t zhash = type_hash(z);
    std::cout << (xhash == yhash) << std::endl; // should be 0
    std::cout << (yhash == zhash) << std::endl; // should be 1
    return 0;
}

我希望type_hash 函数在编译时返回该类型唯一的哈希键。有没有办法在 C++17 或 C++2a 中做到这一点(理想情况下只依赖标准而不依赖编译器内在函数)?

【问题讨论】:

  • (免责声明:非花哨的方式)如果您已经知道类型,则可以只针对感兴趣的类型专门化 type_hash 函数,请确保为每种类型返回唯一值。
  • @Ayub 可惜我事先不知道类型
  • 在某些实现中,&amp;typeid(T) 对于同一类型将始终相同,但这不能保证。不幸的是std::type_info::hash_code()不是constexpr
  • @Vincent 如果有帮助请检查一下wandbox.org/permlink/yp1VWQe1BmyNCcO5
  • 这是小事,但您可能想在第二个 cout 中比较 yz 而不是 xz,因为后面有 yhash == zhash

标签: c++ hash c++17 template-meta-programming c++20


【解决方案1】:

我怀疑纯粹使用标准 C++ 是否可行。


但有一种解决方案适用于大多数主要编译器(至少 GCC、Clang 和 MSVC)。您可以散列以下函数返回的字符串:

template <typename T> constexpr const char *foo()
{
    #ifdef _MSC_VER
    return __FUNCSIG__;
    #else
    return __PRETTY_FUNCTION__;
    #endif
}

【讨论】:

    【解决方案2】:

    我不知道如何为哈希获取 std::size_t

    但是如果你接受一个指向某个东西的指针,也许你可以在模板类中获取一个静态成员的地址。

    我的意思是……如下所示

    #include <iostream>
    #include <type_traits>
    
    template <typename>
    struct type_hash
     {
       static constexpr int          i     { };
       static constexpr int const *  value { &i };
     };
    
    template <typename T>
    static constexpr auto type_hash_v = type_hash<T>::value;
    
    
    int main ()
     {
       auto x = []{};
       auto y = []{};
       auto z = x;
       std::cout << std::is_same_v<decltype(x), decltype(y)> << std::endl; // 0
       std::cout << std::is_same_v<decltype(x), decltype(z)> << std::endl; // 1
       constexpr auto xhash = type_hash_v<decltype(x)>;
       constexpr auto yhash = type_hash_v<decltype(y)>;
       constexpr auto zhash = type_hash_v<decltype(z)>;
       std::cout << (xhash == yhash) << std::endl; // should be 0
       std::cout << (xhash == zhash) << std::endl; // should be 1
     } // ...........^^^^^  xhash, not yhash
    

    如果你真的想要 type_hash 作为一个函数,我想你可以简单地创建一个返回接收到的类型的 type_hash_v&lt;T&gt; 的函数。

    【讨论】:

    • 我喜欢这个想法,但它似乎不起作用:wandbox.org/permlink/HVAoJWlmj7onyapg
    • @MartinMorterol - 感谢您的报告:我之前没有注意到。在我看来,行为是正确的,但最后一个 cout(来自 OP)是错误的:第二个 cout(如果我们想要获得 1)应该比较 xhashzhash,因为 @987654331 @ 和 z 来自同一类型。我已经更正了我的答案。
    • xD 也没有看到。顺便说一句,很好的解决方案!
    • 这是一个聪明的方法。 std::any 实现倾向于使用类似的想法来避免 RTTI。遗憾的是,指针不能reinterpret_casted 到常量求值中的整数。
    【解决方案3】:

    基于HolyBlackCat 的答案,一个constexpr 模板变量,它是一个类型哈希的(简单)实现:

    template <typename T>
    constexpr std::size_t Hash()
    {
        std::size_t result{};
    
    #ifdef _MSC_VER
    #define F __FUNCSIG__
    #else
    #define F __PRETTY_FUNCTION__
    #endif
    
        for (const auto &c : F)
            (result ^= c) <<= 1;
    
        return result;
    }
    
    template <typename T>
    constexpr std::size_t constexpr_hash = Hash<T>();
    

    可以如下图使用:

    constexpr auto f = constexpr_hash<float>;
    constexpr auto i = constexpr_hash<int>;
    

    检查godbolt,这些值确实是在编译时计算的。

    【讨论】:

      【解决方案4】:

      我认为这是不可能的。 “类型独有的哈希键”听起来像是您正在寻找完美的哈希(无冲突)。即使我们忽略 size_t 具有有限数量的可能值,通常我们也无法知道所有类型,因为共享库之类的东西。

      您需要它在两次运行之间保持不变吗?如果没有,您可以设置一个注册方案。

      【讨论】:

        【解决方案5】:

        我同意其他答案,即在标准 C++ 中通常还不可能,但我们可以解决问题的受限版本。

        由于这都是编译时编程,我们不能有可变状态,所以如果你愿意为每个状态变化使用一个新变量,那么这样的事情是可能的:

        • hash_state1 = hash(type1)
        • hash_state2 = hash(type2, hash_state1)
        • hash_state3 = hash(type3, hash_state2)

        “hash_state”实际上只是迄今为止我们散列的所有类型的唯一类型列表。作为散列新类型的结果,它还可以提供size_t 值。 如果我们寻求散列的类型已经存在于类型列表中,我们返回该类型的索引。

        这需要相当多的样板:

        1. 确保类型在类型列表中是唯一的:我在这里使用了@Deduplicator 的答案:https://stackoverflow.com/a/56259838/27678
        2. 在唯一类型列表中查找类型
        3. 使用if constexpr 检查类型是否在类型列表中 (C++17)

        Live Demo


        第 1 部分:一个独特的类型列表:

        再次感谢@Deduplicator's answer here 这部分。由于依赖于 tuple-cat 的实现,以下代码通过在 O(log N) 时间内对类型列表进行查找来节省编译时性能。

        代码的通用性几乎令人沮丧,但好的部分是它允许您使用任何通用类型列表(tuplevariant,自定义的东西)。

        namespace detail {
            template <template <class...> class TT, template <class...> class UU, class... Us>
            auto pack(UU<Us...>)
            -> std::tuple<TT<Us>...>;
        
            template <template <class...> class TT, class... Ts>
            auto unpack(std::tuple<TT<Ts>...>)
            -> TT<Ts...>;
        
            template <std::size_t N, class T>
            using TET = std::tuple_element_t<N, T>;
        
            template <std::size_t N, class T, std::size_t... Is>
            auto remove_duplicates_pack_first(T, std::index_sequence<Is...>)
            -> std::conditional_t<(... || (N > Is && std::is_same_v<TET<N, T>, TET<Is, T>>)), std::tuple<>, std::tuple<TET<N, T>>>;
        
            template <template <class...> class TT, class... Ts, std::size_t... Is>
            auto remove_duplicates(std::tuple<TT<Ts>...> t, std::index_sequence<Is...> is)
            -> decltype(std::tuple_cat(remove_duplicates_pack_first<Is>(t, is)...));
        
            template <template <class...> class TT, class... Ts>
            auto remove_duplicates(TT<Ts...> t)
            -> decltype(unpack<TT>(remove_duplicates<TT>(pack<TT>(t), std::make_index_sequence<sizeof...(Ts)>())));
        }
        
        template <class T>
        using remove_duplicates_t = decltype(detail::remove_duplicates(std::declval<T>()));
        

        接下来,我声明我自己的自定义类型列表以使用上述代码。你们大多数人以前见过的一个非常简单的空结构:

        template<class...> struct typelist{};
        

        第 2 部分:我们的“hash_state”

        “hash_state”,我称之为hash_token

        template<size_t N, class...Ts>
        struct hash_token
        {
            template<size_t M, class... Us>
            constexpr bool operator ==(const hash_token<M, Us...>&)const{return N == M;}
            constexpr size_t value() const{return N;}
        };
        

        简单地为哈希值封装一个size_t(你也可以通过value()函数访问它)和一个比较器来检查两个hash_tokens是否相同(因为你可以有两个不同的类型列表但相同的哈希值. 例如,如果您对int 进行哈希处理以获取令牌,然后将该令牌与您已哈希处理的令牌进行比较(intfloatcharint))。

        第 3 部分:type_hash 函数

        最后我们的type_hash函数:

        template<class T, size_t N, class... Ts>
        constexpr auto type_hash(T, hash_token<N, Ts...>) noexcept
        {
            if constexpr(std::is_same_v<remove_duplicates_t<typelist<Ts..., T>>, typelist<Ts...>>)
            {
                return hash_token<detail::index_of<T, Ts...>(), Ts...>{};
            }
            else
            {
                return hash_token<N+1, Ts..., T>{};
            }
        }
        
        template<class T>
        constexpr auto type_hash(T) noexcept
        {
            return hash_token<0, T>{};
        }
        

        第一个重载是针对泛型的;您已经“散列”了许多类型,并且想要再散列另一种类型。它检查你正在散列的类型是否已经散列,如果是,它返回唯一类型列表中类型的索引。

        为了在类型列表中获取类型的索引,我使用简单的模板扩展来节省一些编译时模板实例化(避免递归查找):

        // find the first index of T in Ts (assuming T is in Ts)
        template<class T, class... Ts>
        constexpr size_t index_of()
        {
            size_t index = 0;
            size_t toReturn = 0;
            using swallow = size_t[];
            (void)swallow{0, (void(std::is_same_v<T, Ts> ? toReturn = index : index), ++index)...};
            
            return toReturn;
        }
        

        type_hash 的第二个重载用于创建从 0 开始的初始 hash_token

        用法:

        int main()
        {
            auto x = []{};
            auto y = []{};
            auto z = x;
            std::cout << std::is_same_v<decltype(x), decltype(y)> << std::endl; // 0
            std::cout << std::is_same_v<decltype(x), decltype(z)> << std::endl; // 1
         
            constexpr auto xtoken = type_hash(x);
            constexpr auto xytoken = type_hash(y, xtoken);
            constexpr auto xyztoken = type_hash(z, xytoken);
            std::cout << (xtoken == xytoken) << std::endl; // 0
            std::cout << (xtoken == xyztoken) << std::endl; // 1
        }
        

        结论:

        在很多代码中并不是很有用,但这可能有助于解决一些受限的元编程问题。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-05-12
          • 1970-01-01
          • 2018-07-04
          • 2011-11-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-06
          相关资源
          最近更新 更多