表达式 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 中了解有关此实用程序及其引入的模式的更多信息。
我正在尝试 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 中了解有关此实用程序及其引入的模式的更多信息。