首页 > 解决方案 > 表达式 SFINAE:如何根据类型是否包含具有一个或多个参数的函数来选择模板版本

问题描述

我试图在编译时根据参数是否实现特定功能在不同模板实现之间进行选择。这是一个常见问题(请参阅this SO questionthis example referenced by this article。常见答案是“使用表达式SFINAE”。

大多数示例显示了表达式 SFINAE 如何用于基于零参数函数的存在进行选择。declval我试图通过使用(松散地基于此示例)使这些适应我的 1 参数用例,但我似乎无法让它工作。

我确定我在下面的示例中做错了什么,但我无法弄清楚它是什么。该示例试图定义一个模板的两个版本,如果它存在,bool Util::Container::Contains(container, value)将使用容器的内置find(value)方法,否则回退到使用线性搜索std::find(...)

请注意:我知道我可以通过为 unordered_map、unordered_set 等重载 Contains() 来完成这项工作,但我想找出这种基于模式的方法,以便它自动委托给任何容器find(value)而不需要要添加的重载。

#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <string>

namespace Util::Container {

    namespace Detail
    {
        template <typename T>
        class HasFindMethod
        {
        private:
            typedef char YesType[1];
            typedef char NoType[2];

            // This is how the examples show it being done for a 0-arg function
            //template <typename C> static YesType& Test(decltype(&C::find));

            // Here's my attempt to make it match a 1-arg function
            template <typename C> static YesType& 
                Test(decltype(std::declval<C>().find(std::declval<const C::value_type&>())));

            template <typename C> static NoType& Test(...);

        public:
            enum { value = sizeof(Test<T>(0)) == sizeof(YesType) };
        };
    }

    // Fallback: uses std::find() to do the lookup if no type-specific T::find(value) exists
    template<typename T>
    bool Contains(const T& in_container, const typename T::value_type& in_item)
    {
        const auto& result = std::find(in_container.cbegin(), in_container.cend(), in_item);
        return (result != in_container.cend());
    }

    // Preferred: use T::find() to do the lookup if possible
    template<typename T>
    inline typename std::enable_if<Detail::HasFindMethod<T>::value, bool>::type 
        Contains(const T& in_container, const typename T::value_type& in_item)
    {
        return (in_container.find(in_item) != in_container.end());
    }
}

int main()
{
    const std::vector<int> v { 1, 2, 3 };
    const std::unordered_map<int, std::string> m { {1,"1" }, {2,"2"} };
    const std::unordered_set<std::string> s { "1" , "2" };

    // These should use the std::find()-based version of Contains() since vector and unordered_map
    // have no find(value_type) method. And they do.
    const bool r_v = Util::Container::Contains(v, 2);
    const bool r_m = Util::Container::Contains(m, { 2, "2" });

    // !!!!!! 
    // 
    // This should use the T::find(value_type)-based version of Contains() since
    // unordered_set has a find(value_type) method.
    //
    // But it doesn't --- that's the issue I'm trying to solve.
    // 
    const bool r_s = Util::Container::Contains(s, "2");
}

如果有人能告诉我如何解决这个问题,我将非常感激。

FWIW,我正在尝试在 Visual Studio 2017 v15.8 中实现它

标签: c++c++11templatesexpressionsfinae

解决方案


一个简单的方法decltype

template<typename C, typename V>
auto Contains(const C& c, const V& value)
-> decltype(std::find(c.cbegin(), c.cend(), value) != c.cend())
{
    return std::find(c.cbegin(), c.cend(), value) != c.cend();
}

template <typename C, typename Key>
auto Contains(const C& c, const Key& key)
-> decltype(c.find(key) != c.end())
{
    return c.find(key) != c.end();
}

但是当这两个功能都可能时,你会有模棱两可的电话。

所以只需添加额外的参数来优先考虑重载:

struct low_priority {};
struct high_priority : low_priority {};


template<typename C, typename V>
auto ContainsImpl(low_priority, const C& c, const V& value)
-> decltype(std::find(c.cbegin(), c.cend(), value) != c.cend())
{
    return std::find(c.cbegin(), c.cend(), value) != c.cend();
}

template <typename C, typename Key>
auto ContainsImpl(high_priority, const C& c, const Key& key)
-> decltype(c.find(key) != c.end())
{
    return c.find(key) != c.end();
}

template <typename C, typename T>
auto Contains(const C& c, const T& t)
-> decltype(ContainsImpl(high_priority{}, c, t))
{
    return ContainsImpl(high_priority{}, c, t);
}

现在关于您的版本,您有几个问题

最后一个:

// Expected Fallback: uses std::find() to do the lookup if no type-specific T::find(value) exists
template<typename T>
bool Contains(const T&, const typename T::value_type&);

// Expected Preferred: use T::find() to do the lookup if possible
template<typename T>
typename std::enable_if<Detail::HasFindMethod<T>::value, bool>::type 
Contains(const T&, const typename T::value_type&);

SFINAE 允许丢弃超载,但不能优先处理它们。你必须使用优先级,如上图,或者创建独占的重载集:

template<typename T>
typename std::enable_if<!Detail::HasFindMethod<T>::value, bool>::type 
Contains(const T&, const typename T::value_type&);

template<typename T>
typename std::enable_if<Detail::HasFindMethod<T>::value, bool>::type 
Contains(const T&, const typename T::value_type&);

除此之外,正如评论中提到的那样,map家庭将使用key_type而不是value_type.

那么你的检测代码有问题,

// 这就是示例显示它是如何为 0-arg 函数完成的 //template static YesType& Test(decltype(&C::find));

不,这会检测是否C有方法find(没有重载)。

template <typename C> static YesType& 
Test(decltype(std::declval<C>().find(std::declval<const C::value_type&>())));

在这里,您使用 SFINAE,但最终类型将是 ( const_) iterator,并且Test<C>(0)不会承受该重载(除非可以构建迭代器,0而不是常规情况)。添加额外*内容是可能的,然后您在迭代器上有指针,该指针可能由0.

否则,您可以使用您提供的链接中提供的代码:

namespace detail{
  template<class T, typename ... Args>
  static auto test_find(int)
      -> sfinae_true<decltype(std::declval<T>().find(std::declval<const Arg&>()...))>;
  template<class, class ...>
  static auto test_find(long) -> std::false_type;
} // detail::

template<class C, typename ... Args>
struct has_find : decltype(detail::test_find<T, Args...>(0)){};
// int has higher priority than long for overload resolution

然后将您的特征与std::enable_if has_find<Container, Key>::value.


推荐阅读