表达式 SFINAE:如何根据类型是否包含具有一个或多个参数的函数来 select 模板版本

Expression SFINAE: how to select template version based on whether type contains a function with one or more arguments

我正在尝试 select 在不同模板实现之间的编译时,这取决于参数是否实现了特定功能。这是一个常见问题(参见 this S.O. question and this example referenced by this article。常见答案是 "use expression SFINAE"。

大多数示例展示了表达式 SFINAE 如何用于 select 基于零参数函数的存在。我试图通过使用 declval(松散地基于 this example)使它们适应我的 1 参数用例,但我似乎无法让它工作。

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

请注意: 我知道我可以通过为 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

中实现它

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

那你的检测代码有问题,

// This is how the examples show it being done for a 0-arg function //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.

初始化

否则您可以使用您提供的 link:

中提供的代码
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.

一起使用

眼前的问题是您传递给 Test 的参数与 YesType 版本不兼容。

例如,Detail::HasFindMethod<std::unordered_set<int>> 将产生以下两个 Test 签名(因为 find 会 return 和 iterator):

        static YesType& Test(std::unordered_set<int>::iterator);

        static NoType& Test(...);

您尝试使用无法转换为 iterator 的参数 0 调用 Test。因此,选择了第二个。

作为解决方案,使用指针:

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

然后使用 nullptr 参数进行检查:

        enum { value = sizeof(Test<T>(nullptr)) == sizeof(YesType) };

现在我们会有歧义(Test(...) 也会匹配),所以我们可以使那个匹配更差:

        template <typename C, class ... Args> static NoType& Test(void*, Args...);

如其他答案所示,这仍然是一个相对复杂的解决方案(并且还有更多问题阻止它在您的实例中工作,例如 enable_if 工作时重载之间的歧义) .只是在此处解释您尝试中的特定塞子。

使用 void_t 实用程序可以实现更简单(在我看来)和更易读的解决方案:

template <typename T, typename Dummy = void>
struct has_member_find : std::false_type { };

template <typename T>
struct has_member_find<T,
    std::void_t<decltype(std::declval<T>().find(std::declval<typename T::value_type &>()))>>
    : std::true_type { };

template<typename T>
std::enable_if_t<!has_member_find<T>::value, 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());
}

template<typename T>
std::enable_if_t<has_member_find<T>::value, bool>
Contains(const T& in_container, const typename T::value_type& in_item)
{
    return (in_container.find(in_item) != in_container.end());
}

请注意,void_t 仅在 C++17 之后可用,但是如果您没有完整的 C++17 支持,您可以自己定义它,因为它的定义非常简单:

template< class... >
using void_t = void;

您可以在 this paper 中了解有关此实用程序及其引入的模式的更多信息。