【问题标题】:How to use type_traits to generate code dependent on if a class specialisation exists?如何使用 type_traits 生成依赖于类专业化是否存在的代码?
【发布时间】:2013-12-26 17:08:37
【问题描述】:

背景

我正在尝试编写一个class template Hasher,它将以两种不同的方式实现,具体取决于是否为 T 实现了std::hash<T>

template<typename T>
struct Hasher
{
  std::size_t hash( T t ) const;
  // implement as A { std::hash<T> h; return h( t ); }
  //           or B { std::hash<std::string> h; return h( t.to_string() );  }
};

如果std::hash&lt;T&gt;已经专门化了,我想用它。如果没有,我希望T 有一个to_string() 函数来返回一个密钥供我散列。

例如,根据cppreference,如果Tlong long、指针或std::string,我想要版本A。如果它不是列出的标准之一,并且如果用户没有专门针对他自己的类型的std::hash&lt;T&gt;,我希望T 有一个std::string to_string() const 给我打电话。在这种情况下,我想生成版本 B。

问题

如何使用 C++11/type_traits/no-SFINAE 生成正确的实现?

附录

另一种思考方式:

这几乎就像我希望版本 B 成为默认行为(即,如果不存在专门化,则使用版本 B)。

测试 NAWAZ 的解决方案

我刚刚在 gcc 4.8.1 上试用了 Nawaz 的解决方案,因为他首先出现,实际上是我最容易阅读和理解的(更重要的)。

#include <functional>
#include <cassert>

template<typename T>
class Hasher
{
        // overloading rules will select this one first...  ...unless it's not valid
        template<typename U>
        static auto hash_impl(U const& u, int)
                -> decltype(std::hash<U>().operator()( u ))
        {
                 return std::hash<U>().operator()( u );
        }
        // as a fallback, we will pick this one
        template<typename U>
        static auto hash_impl(U const& u, ... )
                -> std::size_t
        {
                 return std::hash<std::string>().operator()(u.to_string());
        }

public:
        auto hash( T t ) const -> decltype( hash_impl(t,0) )
        {
                 return hash_impl( t, 0 );
        }
};

struct Foo
{
        std::string  m_id;
        std::string  to_string() const { return m_id; }
};

int
main( int argc, char** argv )
{
        std::string        s{ "Bar" };
        Foo                f{ s     };
        long long          l{ 42ll  };

        Hasher<long long>   hl;
        Hasher<Foo>         hf;
        Hasher<std::string> hs;

        assert( hl.hash( l )==l );
        assert( hf.hash( f )==hs.hash( s ));

        return 0;
}

测试丹尼尔·弗雷的解决方案

Daniel 的实现也很有趣。通过首先计算我们是否有哈希,我可以使用标签调度来选择我想要的实现。我们有一个很好的模式/关注点分离,这导致代码非常干净。

然而,在has_hash&lt;&gt; 的实现中,decltype 的参数一开始让我很困惑。事实上,它不应该被解读为论点。相反,它是一个表达式列表(逗号分隔的表达式)。我们需要遵守here所表达的规则。

C++ 确保每个表达式都被计算并且它的边 效果发生。但是,整个逗号分隔的值 表达式只是最右边表达式的结果。

另外,void() 的使用起初对我来说是个谜。当我将其更改为 double() 以查看会发生什么时,很清楚为什么它真的应该是 void()(因此我们不需要传入第二个模板参数)。

#include <functional>
#include <cassert>

template< typename, typename = void >
struct has_hash
  : std::false_type {};

template< typename T >
struct has_hash< T, decltype( std::hash< T >()( std::declval< T >() ), void() ) >
  : std::true_type {};

template<typename T>
class Hasher
{
        static std::size_t hash_impl(T const& t, std::true_type::type )
        {
                 return std::hash<T>().operator()( t );
        }

        static std::size_t hash_impl(T const& t, std::false_type::type )
        {
                 return std::hash<std::string>().operator()(t.to_string());
        }

public:
        std::size_t hash( T t ) const
        {
                 return hash_impl( t, typename has_hash<T>::type() );
        }
};

struct Foo
{
        std::string  m_id;
        std::string  to_string() const { return m_id; }
};

int
main( int argc, char** argv )
{
        std::string        s{ "Bar" };
        Foo                f{ s     };
        long long          l{ 42ll  };

        Hasher<long long>   hl;
        Hasher<Foo>         hf;
        Hasher<std::string> hs;

        assert( hl.hash( l )==l );
        assert( hf.hash( f )==hs.hash( s ));

        return 0;
}

【问题讨论】:

    标签: c++ templates c++11 typetraits


    【解决方案1】:

    您可以使用 C++11 引入的Expression SFINAE

    这是一个如何实现的示例:

    template<typename T>
    struct Hasher
    {
        auto hash( T t ) const -> decltype(hash_impl(t,0))
        {
           return hash_impl(t, 0);
        }
      private:
        template<typename U>
        static auto hash_impl(U const & u, int) -> decltype(std::hash<U>().hash(u))
        {
           return std::hash<U>().hash(u);
        }
        template<typename U>
        static auto hash_impl(U const & u, ... ) -> std::string
        {
           return u.to_string();
        }
    };
    

    注意hash_impl 是一个重载的函数模板。所以当你写这个时:

           return hash_impl(t, 0); 
    

    由于第二个参数0int,上述首先 尝试调用使用std::hashhash_impl — 如果@,此尝试可能会失败987654328@ 不是有效的表达式(Expression SFINAE)。如果失败,则调用第二个hash_impl

    【讨论】:

    • 顺序错误? OP wanys hash 如果它存在,并且仅当它失败 to_string?
    • @Yakk:哦,是的。固定的。 :-)
    • 我很想坚持 another std::hash&lt;T&gt;().hash(u) 作为第二个后备,所以如果两者都不存在,编译器错误会告诉您专门化 hash 而不是添加to_string 成员。
    • @MooingDuck 让to_string 使用double(或sink),第二个std::hash&lt;T&gt;().hash(u) 使用...? ;)
    【解决方案2】:

    您可以测试是否可以使用相关类型调用 std::hash&lt;T&gt;()(...)。如果是这样,您将从 decltype() 中返回一个类型,该类型可用于 SFINAE 表达式以确定返回类型的大小:

    template <typename T>
    struct has_hash
    {
        template <typename S>
        static char (&test(S*, decltype(std::hash<S>()(std::declval<T&>()))* = 0))[1];
        static char (&test(...))[2];
        static constexpr bool value = 1 == sizeof(test(static_cast<T*>(0)));
    };
    

    基于此,您可以使用has_hash&lt;T&gt;::value 来确定是否已经定义了可用的哈希函数。

    【讨论】:

    • 为了可读性,我可能会把它放在一个宏中,如果不重用的话。
    • @MooingDuck:加油!这段代码并不难阅读……好吧,很少有人定期声明函数返回对数组的引用,std::declval&lt;T&gt;() 在普通代码中可能也很少见。如果这样的东西是以某种形式内置的语言,那就太酷了。另一方面,有很多奇怪的东西已经可以测试了,如果有内置的语言是一个有趣的问题......
    • 您可以让重载返回 std::true_type 和 std::false_type ,这会使一切变得更好。像大蒜一样。
    【解决方案3】:

    如果你需要测试一个表达式是否有效,我更喜欢下面的实现:

    template< typename, typename = void >
    struct has_hash
      : std::false_type {};
    
    template< typename T >
    struct has_hash< T, decltype( std::hash< T >()( std::declval< T >() ), void() ) >
    //                            ^^ expression here ------------------^^
      : std::true_type {};
    

    Live example

    【讨论】:

    • +1 我喜欢这个实现的简洁性,但是你能向我解释一下 decltype() 内部发生了什么吗?它是否只是不断地从左到右评估表达式,直到找到一个有效的表达式?
    • @kfmfe04: decltype 返回表达式的类型,这里使用逗号运算符:第一个表达式用于SFINAE,最后一个表达式用于结果类型(void)。如果任何表达式无效,则模板特化被丢弃。
    • @Jarod42 ty - 我在这里找到了逗号分隔表达式的一个很好的解释:hotscripts.com/forums/c-c/… 我以前使用过它们,但没有意识到整个表达式具有最后一个 sub 的值-表达。另外,我玩弄了 Daniel Frey 的 Live 示例,以了解为什么我们需要一个 void() 而不是 double() 或其他东西。当我有时间时,我会尝试使用这个实现编写一个版本并将其添加到 OP 中。
    • @kfmfe04: 表达式应该在这里返回void,因为根据通用版本has_hash&lt;T&gt; 实际上是has_hash&lt;T, void&gt; 与默认模板值。所以我们必须专攻has_hash&lt;T, void&gt;
    • @kfmfe04 Jarod 已经提供了一些很好的解释,所以我想补充一点,一旦您了解了它的工作原理,该技术既可扩展又具有良好的信噪比。把它放在你的包里,以备不时之需:)
    【解决方案4】:

    又一个实现

    首先,一些样板文件。 type_sinkTypeSink 让我们评估类型并丢弃它们。

    template<typename... T> struct type_sink {typedef void type;};
    template<typename... T> using TypeSink = typename type_sink<T>::type;
    

    然后我们编写一个使用 SFINAE 的非常简单的has_hash。默认选择是“无哈希”,并且当 std::hash&lt;T&gt;()( t )T 类型的变量 t 的有效表达式时,特化是有效的:

    template<typename T, typename=void> struct has_hash:std::false_type {};
    template<typename T> struct has_hash<
      T, TypeSink< decltype( std::hash<T>()( std::declval<T&>() ) ) >
    >: std::true_type {}; 
    

    然后我们编写通用哈希器:

    template<typename T>
    struct Hasher
    {
    private:
      typedef has_hash< typename std::decay<T>::type > test_for_hash;
    public:
      std::size_t operator()( T t ) const {
        return hash( std::forward<T>(t), test_for_hash() );
      }
    private:
      std::size_t hash( T t, std::true_type has_hash ) const {
        return std::hash<T>()( std::forward<T>(t) );
      }
      std::size_t hash( T t, std::false_type no_hash ) const {
        // TODO: static_assert that t.to_string exists, and if not give a useful error message
        return std::hash<std::string>()( std::forward<T>(t).to_string() )
      }
    };
    

    我们使用has_hash trait 对“直接使用hash”或“to_string 然后hash”进行标记调度。

    我们可以做更多层的这种调度。

    我冒昧地让它有点移动感知,因为 T 可以是 T&amp;T const&amp;T 并且它的行为合理。 (我不知道 T&amp;&amp; 在我的脑海中)。

    正如在其他 cmets 中所指出的,可以进行一些工作以生成更好的错误消息。我们希望编译器的错误消息抱怨缺少hash&lt;T&gt; 实现,而不是to_string 实现。这将涉及编写 has_to_string 特征类并执行另一层标记调度,或执行 static_assert 以生成有用的消息,告诉最终用户实现 hash&lt;T&gt; 特化或 T::to_string

    另一种选择是制作通用哈希器:

    template<typename T>
    struct Hasher
    {
    private:
      typedef has_hash< T > test_for_hash;
    public:
      template<typename U>
      std::size_t operator()( U&& u ) const {
        return hash( std::forward<U>(u), test_for_hash() );
      }
    private:
      template<typename U>
      std::size_t hash( U&& u, std::true_type has_hash ) const {
        return std::hash<T>()( std::forward<U>(u) ); // conversion occurs here
      }
      template<typename U>
      std::size_t hash( U&& u, std::false_type no_hash ) const {
        // TODO: static_assert that t.to_string exists, and if not give a useful error message
        T t = std::forward<U>(u); // conversion occurs here -- note, implicit on purpose!
        return std::hash<std::string>()( std::move(t).to_string() )
      }
    };
    

    将转换到T 推迟到最后一刻。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-01
      • 2016-05-28
      • 2017-10-29
      • 2013-03-08
      • 1970-01-01
      • 1970-01-01
      • 2015-07-18
      相关资源
      最近更新 更多