简体   繁体   English

带有std :: enable_if的模板专业化和SFINAE之间的区别?

[英]Difference between template specialization and SFINAE with std::enable_if?

If I have a template function in C++, and want it to behave in a different manner in presence of a specific template parameter, I will use a template specialization:如果我在 C++ 中有一个模板 function,并希望它在存在特定模板参数时以不同的方式表现,我将使用模板特化:

#include <iostream>
#include <type_traits>

template<typename T> void myFunction(T&& input) {
    std::cout << "The parameter is " << input << '\n';
}

template<> void myFunction<int>(int&& input) {
    std::cout << "Double of the parameter is " << 2 * input << '\n';
}

int main() {
    myFunction(24.2);
    myFunction(24);
    myFunction("Hello");
}

Use of std::enable_if<bool B> from <type_traits> enables to activate a similar behaviour for some specific template types.使用<type_traits>中的std::enable_if<bool B>可以激活某些特定模板类型的类似行为。 Any other type would cause a substitution error.任何其他类型都会导致替换错误。

#include <iostream>
#include <type_traits>

template<typename T, std::enable_if_t<std::is_integral_v<T>>* = nullptr > 
void myFunction(T&& input) {
    std::cout << "Double of the parameter is " << 2 * input << '\n';
}

template<typename T, std::enable_if_t<std::is_floating_point_v<T>>* = nullptr > 
void myFunction(T&& input) {
    std::cout << "The parameter is " << input << '\n';
}

int main() {
    myFunction(24.2);
    myFunction(24);
    // myFunction("Hello"); // compile-time substitution error
}

My question is: what is really the gain of using the fancier std::enable_if specialization, in place of the standard template specialization?我的问题是:使用更高级的std::enable_if专业化代替标准模板专业化的真正好处是什么? Are they really alternatives, or std::enable_if is exclusively for template metaprogramming?它们真的是替代品,还是std::enable_if专门用于模板元编程?

With std::enable_if<> , you can write arbitrary conditions.使用std::enable_if<> ,您可以编写任意条件。 An example is specializing based on the size of the object;一个例子是根据 object 的大小进行专门化; other examples include whether a field / member function exists, whether the result of a static constexpr function is what's expected.其他示例包括字段/成员 function 是否存在, static constexpr function 的结果是否符合预期。 Specialization is limited to the type matching.专业化仅限于类型匹配。

These 2 approaches are executed on different stages.这两种方法在不同的阶段执行。 First is overload resolution - on this stage template specialization is not used.首先是重载决议——在这个阶段不使用模板特化。 Only generic template function prototype is in use.仅使用通用模板 function 原型。

On the other hand SFINAE approach drops the overloads that failed to be substituted silently on this stage leaving only one candidate based on enable_if conditions.另一方面,SFINAE 方法丢弃了未能在此阶段静默替换的重载,仅根据 enable_if 条件留下一个候选者。

Second step is template instantiation - here the body of your function is created - and it will use body provided by most specialized template.第二步是模板实例化——这里创建了 function 的主体——它将使用大多数专业模板提供的主体。

tl;dr tl;博士

SFINAE also helps misuse of template entities result in more readable errors. SFINAE 还有助于滥用模板实体导致更多可读错误。

A bit longer有点长

Not using SFINAE but just relying on specializations means, among other things, that you are leaving an unrestrained primary template available for overload resolution, which can result in very lengthly, and not easily readable errors, if it gets fed with types which the implementation doesn't make sense.不使用 SFINAE 而是仅仅依赖特化意味着,除其他外,您将留下一个无限制的主模板可用于重载解决方案,如果它获得了实现不提供的类型,这可能会导致非常冗长且不易阅读的错误没道理。

Indeed, the template will always be instantiated thanks to template type deduction, but then, if the type doesn't happen to be suitable for the implementation of that function.实际上,由于模板类型推导,模板将始终被实例化,但是,如果该类型碰巧不适合实现该 function。 That will be discovered maybe deep in several nested function calls.这可能会在几个嵌套的 function 调用中发现。

As noted in another answer, SFINAE allows one to discard some overloads (SFINAE them out, somebody says) before overload resolution actually takes place, hopefully leaving much less overloads standing, for the benefit of programmer who will read shorter errors.正如在另一个答案中所指出的,SFINAE 允许人们在重载决议实际发生之前丢弃一些重载(有人说,SFINAE 将它们排除在外),希望留下更少的重载,以使程序员能够阅读更短的错误。


Example例子

This is a not-too-far from real-life example of how template errors can be long, but made shorter thanks to SFINAE.这是一个与现实生活中的示例相距不远的示例,说明模板错误可能很长,但由于 SFINAE 而变得更短。

In the toy example below, I've defined a template function for inserting an element in a collection.在下面的玩具示例中,我定义了一个模板 function 用于在集合中插入元素。 A few details to better understand why I wrote it the way I did:一些细节可以更好地理解我为什么要这样写:

  • The insertions of an element in a std::unordered_set<T, whatever> requires that operator== is defined on two arguments of type T ;std::unordered_set<T, whatever>中插入元素要求operator==在两个T类型的 arguments 上定义; as you can see from the commented friend function, I've intentionally not defined it for the type A , so as to make the code fail at compile time (if you uncomment that line the code compiles and runs smoothly).正如您从评论的friend function 中看到的那样,我故意没有为类型A定义它,以便使代码在编译时失败(如果您取消注释该行代码编译并运行顺利)。

    • You can disregard the silly hash function;你可以无视傻hash function; it's there just to make the code compile in case you uncomment the friend definition.它只是为了使代码编译以防您取消注释friend定义。
  • I've defined an Equatable concept to check whether operator== is defined on two arguments of a given type T , ie if the type is suitable for the element type of an unordered_set ;我定义了一个Equatable概念来检查是否在给定类型T的两个 arguments 上定义了operator== ,即该类型是否适合unordered_set的元素类型; this was to have a predicate to apply to a type as the condition for an enable_if .这是有一个谓词应用于类型作为enable_if的条件。

  • myInsert is the function object of the question, a template function which is unrestricted. myInsert是问题的 function object,模板 function 不受限制。

    • You can also imagine you've written a specialization for another class B on which operator== is defined, but the point is that among all other types you can throw at it, there's A , on which operator== is not defined, and this is causing the error.您还可以想象您已经为另一个 class B编写了一个特化,在该 B定义了operator== ,但重点是在所有其他类型中您可以抛出它,有A ,在其未定义operator== ,并且导致了错误。
#include <cmath>
#include <type_traits>
#include <unordered_set>

// concept to detect whether operator== is defined argument of a type T
template<typename T, typename = void>
struct Equatable : std::false_type {};
template<typename T>
struct Equatable<T, std::void_t<decltype(T{} == T{})>> : std::true_type {};
template<typename T>
constexpr bool equatable_v = Equatable<T>::value;

// The problematic class which has no operator== defined on it
struct A {
    void empty(int) {
    }
    //friend bool operator==(A const&, A const&) { return true; }
};

// Our template function not using SFINAE
template<typename Coll, typename Elem>
void myInsert(Coll& coll, Elem&& elem) {
    coll.insert(std::move(elem));
}

int main() {
    auto hash = [](A const&){ return 1; };
    std::unordered_set<A, decltype(hash)> s;
    myInsert(s, A{});
}

Try to compile the code above.尝试编译上面的代码。 You'll most likely get an error which is long with respect to compile-time errors of non-templated code (but believe me, it's short! I've actually included <cmath> for the purpose of triggering more instantiation attempts to make the error a bit longer).对于非模板化代码的编译时错误,您很可能会得到一个较长的错误(但相信我,它很短!我实际上包含<cmath>是为了触发更多实例化尝试以使错误更长一点)。 Here's what I get:这是我得到的:

In file included from /usr/include/c++/12.2.0/unordered_set:44,
                 from example.cpp:3:
/usr/include/c++/12.2.0/bits/stl_function.h: In instantiation of ‘constexpr bool std::equal_to<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = A]’:
/usr/include/c++/12.2.0/bits/hashtable_policy.h:1701:18:   required from ‘bool std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _Traits>::_M_key_equals_tr(const _Kt&, const std::__detail::_Hash_node_value<_Value, typename _Traits::__hash_cached::value>&) const [with _Kt = A; _Key = A; _Value = A; _ExtractKey = std::__detail::_Identity; _Equal = std::equal_to<A>; _Hash = main()::<lambda(const A&)>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits<true, true, true>; typename _Traits::__hash_cached = std::__detail::_Hashtable_traits<true, true, true>::__hash_cached]’
/usr/include/c++/12.2.0/bits/hashtable.h:2237:32:   required from ‘std::pair<typename std::__detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::iterator, bool> std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::_M_insert_unique(_Kt&&, _Arg&&, const _NodeGenerator&) [with _Kt = A; _Arg = A; _NodeGenerator = std::__detail::_AllocNode<std::allocator<std::__detail::_Hash_node<A, true> > >; _Key = A; _Value = A; _Alloc = std::allocator<A>; _ExtractKey = std::__detail::_Identity; _Equal = std::equal_to<A>; _Hash = main()::<lambda(const A&)>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, true, true>; typename std::__detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::iterator = std::__detail::_Insert_base<A, A, std::allocator<A>, std::__detail::_Identity, std::equal_to<A>, main()::<lambda(const A&)>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >::iterator; typename _Traits::__constant_iterators = std::__detail::_Hashtable_traits<true, true, true>::__constant_iterators]’
/usr/include/c++/12.2.0/bits/hashtable.h:906:27:   required from ‘std::pair<typename std::__detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::iterator, bool> std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::_M_insert(_Arg&&, const _NodeGenerator&, std::true_type) [with _Arg = A; _NodeGenerator = std::__detail::_AllocNode<std::allocator<std::__detail::_Hash_node<A, true> > >; _Key = A; _Value = A; _Alloc = std::allocator<A>; _ExtractKey = std::__detail::_Identity; _Equal = std::equal_to<A>; _Hash = main()::<lambda(const A&)>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, true, true>; typename std::__detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits>::iterator = std::__detail::_Insert_base<A, A, std::allocator<A>, std::__detail::_Identity, std::equal_to<A>, main()::<lambda(const A&)>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >::iterator; typename _Traits::__constant_iterators = std::__detail::_Hashtable_traits<true, true, true>::__constant_iterators; std::true_type = std::integral_constant<bool, true>]’
/usr/include/c++/12.2.0/bits/hashtable_policy.h:1035:22:   required from ‘std::__detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits, true>::__ireturn_type std::__detail::_Insert<_Key, _Value, _Alloc, _ExtractKey, _Equal, _Hash, _RangeHash, _Unused, _RehashPolicy, _Traits, true>::insert(value_type&&) [with _Key = A; _Value = A; _Alloc = std::allocator<A>; _ExtractKey = std::__detail::_Identity; _Equal = std::equal_to<A>; _Hash = main()::<lambda(const A&)>; _RangeHash = std::__detail::_Mod_range_hashing; _Unused = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits<true, true, true>; __ireturn_type = std::__detail::_Insert<A, A, std::allocator<A>, std::__detail::_Identity, std::equal_to<A>, main()::<lambda(const A&)>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true>, true>::__ireturn_type; value_type = A]’
/usr/include/c++/12.2.0/bits/unordered_set.h:426:27:   required from ‘std::pair<typename std::_Hashtable<_Value, _Value, _Alloc, std::__detail::_Identity, _Pred, _Hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<std::__not_<std::__and_<std::__is_fast_hash<_Hash>, std::__is_nothrow_invocable<const _Hash&, const _Tp&> > >::value, true, true> >::iterator, bool> std::unordered_set<_Value, _Hash, _Pred, _Alloc>::insert(value_type&&) [with _Value = A; _Hash = main()::<lambda(const A&)>; _Pred = std::equal_to<A>; _Alloc = std::allocator<A>; typename std::_Hashtable<_Value, _Value, _Alloc, std::__detail::_Identity, _Pred, _Hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<std::__not_<std::__and_<std::__is_fast_hash<_Hash>, std::__is_nothrow_invocable<const _Hash&, const _Tp&> > >::value, true, true> >::iterator = std::__detail::_Insert_base<A, A, std::allocator<A>, std::__detail::_Identity, std::equal_to<A>, main()::<lambda(const A&)>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >::iterator; value_type = A]’
example.cpp:28:16:   required from ‘void myInsert(Coll&, Elem&&) [with Coll = std::unordered_set<A, main()::<lambda(const A&)> >; Elem = A]’
example.cpp:34:13:   required from here
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: error: no match for ‘operator==’ (operand types are ‘const A’ and ‘const A’)
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~
In file included from /usr/include/c++/12.2.0/bits/stl_algobase.h:67,
                 from /usr/include/c++/12.2.0/bits/specfun.h:45,
                 from /usr/include/c++/12.2.0/cmath:1935,
                 from example.cpp:1:
/usr/include/c++/12.2.0/bits/stl_iterator.h:530:5: note: candidate: ‘template<class _IteratorL, class _IteratorR> constexpr bool std::operator==(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorR>&) requires requires{{std::operator==::__x->base() == std::operator==::__y->base()} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];}’ (reversed)
  530 |     operator==(const reverse_iterator<_IteratorL>& __x,
      |     ^~~~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:530:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: note:   ‘const A’ is not derived from ‘const std::reverse_iterator<_IteratorL>’
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:1656:5: note: candidate: ‘template<class _IteratorL, class _IteratorR> constexpr bool std::operator==(const move_iterator<_IteratorL>&, const move_iterator<_IteratorR>&) requires requires{{std::operator==::__x->base() == std::operator==::__y->base()} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];}’ (reversed)
 1656 |     operator==(const move_iterator<_IteratorL>& __x,
      |     ^~~~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:1656:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: note:   ‘const A’ is not derived from ‘const std::move_iterator<_IteratorL>’
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~
In file included from /usr/include/c++/12.2.0/unordered_set:40:
/usr/include/c++/12.2.0/bits/allocator.h:219:5: note: candidate: ‘template<class _T1, class _T2> constexpr bool std::operator==(const allocator<_Up>&, const allocator<_T2>&)’ (reversed)
  219 |     operator==(const allocator<_T1>&, const allocator<_T2>&)
      |     ^~~~~~~~
/usr/include/c++/12.2.0/bits/allocator.h:219:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: note:   ‘const A’ is not derived from ‘const std::allocator<_Up>’
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~
In file included from /usr/include/c++/12.2.0/bits/stl_algobase.h:64:
/usr/include/c++/12.2.0/bits/stl_pair.h:640:5: note: candidate: ‘template<class _T1, class _T2> constexpr bool std::operator==(const pair<_T1, _T2>&, const pair<_T1, _T2>&)’
  640 |     operator==(const pair<_T1, _T2>& __x, const pair<_T1, _T2>& __y)
      |     ^~~~~~~~
/usr/include/c++/12.2.0/bits/stl_pair.h:640:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: note:   ‘const A’ is not derived from ‘const std::pair<_T1, _T2>’
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:589:5: note: candidate: ‘template<class _Iterator> constexpr bool std::operator==(const reverse_iterator<_IteratorL>&, const reverse_iterator<_IteratorL>&) requires requires{{std::operator==::__x->base() == std::operator==::__y->base()} -> decltype(auto) [requires std::convertible_to<<placeholder>, bool>];}’
  589 |     operator==(const reverse_iterator<_Iterator>& __x,
      |     ^~~~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:589:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: note:   ‘const A’ is not derived from ‘const std::reverse_iterator<_IteratorL>’
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:1726:5: note: candidate: ‘template<class _Iterator> constexpr bool std::operator==(const move_iterator<_IteratorL>&, const move_iterator<_IteratorL>&)’
 1726 |     operator==(const move_iterator<_Iterator>& __x,
      |     ^~~~~~~~
/usr/include/c++/12.2.0/bits/stl_iterator.h:1726:5: note:   template argument deduction/substitution failed:
/usr/include/c++/12.2.0/bits/stl_function.h:378:20: note:   ‘const A’ is not derived from ‘const std::move_iterator<_IteratorL>’
  378 |       { return __x == __y; }
      |                ~~~~^~~~~~

I challenge you to read it and tell me it's easy to understand what's telling us.我挑战你阅读它并告诉我它很容易理解告诉我们的内容。

What's the alternative?有什么选择? Using std::enable_if to enforce the condition that the types fed to myInsert have to satisfy.使用std::enable_if强制提供给myInsert的类型必须满足的条件。 A simplified and incomplete attempt consists in changing the line一个简化且不完整的尝试包括更改线路

template<typename Coll, typename Elem>

to

template<typename Coll, typename Elem, std::enable_if_t<equatable_v<Elem>> = 0>

which results in the error to become much shorter:这导致错误变得更短:

example.cpp: In function ‘int main()’:
example.cpp:34:13: error: no matching function for call to ‘myInsert(std::unordered_set<A, main()::<lambda(const A&)> >&, A)’
   34 |     myInsert(s, A{});
      |     ~~~~~~~~^~~~~~~~
example.cpp:27:6: note: candidate: ‘template<class Coll, class Elem, typename std::enable_if<equatable_v<Elem>, int>::type <anonymous> > void myInsert(Coll&, Elem&&)’
   27 | void myInsert(Coll& coll, Elem&& elem) {
      |      ^~~~~~~~
example.cpp:27:6: note:   template argument deduction/substitution failed:
example.cpp:23:46: error: no type named ‘type’ in ‘struct std::enable_if<false, int>’
   23 | , std::enable_if_t<equatable_v<Elem>, int> = 0>
      |                                              ^

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM