如何使用type_traits生成依赖于类专门化的代码?

时间:2013-12-26 17:08:38

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

背景

我正在尝试编写一个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<T>已经专门化,我想使用它。如果没有,我希望T有一个to_string()函数来返回一个键供我哈希。

例如,根据cppreference,如果Tlong long,指针或std::string,我想要版本A.如果它不是那些标准之一列出的内容以及如果用户没有专门为自己的类型设置std::hash<T>,我希望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 FREY&S解决方案

丹尼尔的实施也非常有趣。通过首先计算我们是否有哈希,我能够使用tag-dispatch来选择我想要的实现。我们有一个很好的模式/关注点分离,这导致了非常干净的代码。

然而,在has_hash<>的实现中,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;
}

4 个答案:

答案 0 :(得分:10)

您可以使用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,上述第一个 尝试调用使用{{1}的hash_impl } - 如果std::hash不是有效表达式(表达式SFINAE ),则此尝试可能会失败。如果失败,则调用第二个std::hash<U>().hash(u)

答案 1 :(得分:5)

您可以测试是否可以使用相关类型调用std::hash<T>()(...)。如果是这样,您将从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<T>::value来确定是否已经定义了可用的哈希函数。

答案 2 :(得分:5)

如果您需要测试表达式是否有效,我更喜欢以下实现:

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

答案 3 :(得分:2)

又一个实现

首先,一些样板。 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<T>()( 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特征对“直接使用hash”或“to_string然后hash”进行标记调度。

我们可以进行更多层次的调度。

我冒昧地让它有点移动意识,因为T可以是T&T const&T,而且行为合理。 (我不知道我头顶的T&&。)

如其他评论中所述,可以完成一些生成更好的错误消息的工作。我们希望编译器的错误消息抱怨缺少hash<T>实现而不是to_string实现。这将涉及编写has_to_string traits类并执行另一层标记调度,或执行static_assert生成有用的消息,告知最终用户实现hash<T>特化或{ {1}}。

另一种选择是建立一个通用的哈希:

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() ) } }; 直到最后一刻。