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

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

本文关键字:函数 参数 选择 版本 何根 SFINAE 类型 有一个 包含具 是否 表达式      更新时间:2023-10-16

我试图在编译时根据参数是否实现特定函数在不同的模板实现之间进行选择。这是一个常见问题(请参阅此 S.O. 问题和本文引用的此示例。常见的答案是"使用表达式SFINAE"。

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

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

请注意:我知道我可以通过重载 Contains(( 来完成这项工作unordered_map、unordered_set等,但相反,我想弄清楚这种基于模式的方法,以便它会自动委托给任何容器的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

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

这就是示例显示它为 0-arg 函数完成的方式 模板静态 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_ifhas_find<Container, Key>::value一起使用.

直接的问题是你传递给Test的参数与YesType版本不兼容。

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

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

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

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

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;

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