首先,我认为 SFINAE 通常应该对接口隐藏。它使界面混乱。将 SFINAE 远离水面,并使用标签调度来挑选过载。
其次,我什至从特征类中隐藏了 SFINAE。根据我的经验,编写“我可以做 X”代码很常见,我不想编写凌乱的 SFINAE 代码来做到这一点。因此,我改为编写一个通用can_apply
特征,并且如果使用decltype
.
然后,我们将 SFIANE 失败decltype
特征提供给can_apply
,并根据应用程序是否失败得出真/假类型。
这将每个“我可以做 X”特征的工作量减少到最低限度,并使有些棘手和脆弱的 SFINAE 代码远离日常工作。
我使用 C++1z 的void_t
. 自己实现它很容易(在这个答案的底部)。
正在为 C++1z 中的标准化提出一个类似的元函数can_apply
,但它不像现在那样稳定void_t
,所以我没有使用它。
首先,一个details
命名空间来隐藏其实现,can_apply
以免被意外发现:
namespace details {
template<template<class...>class Z, class, class...>
struct can_apply:std::false_type{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
std::true_type{};
}
然后我们可以用 来写can_apply
,details::can_apply
它有一个更好的界面(它不需要void
传递额外的内容):
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;
以上是通用辅助元编程代码。一旦我们有了它,我们就可以can_to_string
非常干净地编写一个特征类:
template<class T>
using to_string_t = decltype( std::to_string( std::declval<T>() ) );
template<class T>
using can_to_string = can_apply< to_string_t, T >;
我们有一个can_to_string<T>
真实的特征,如果我们可以to_string
a T
。
现在需要编写一个像这样的新特征的工作是 2-4 行简单的代码——只需创建一个decltype
using
别名,然后can_apply
对其进行测试。
一旦我们有了它,我们就使用标签调度到正确的实现:
template<typename T>
std::string stringify(T t, std::true_type /*can to string*/){
return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type /*cannot to string*/){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
template<typename T>
std::string stringify(T t){
return stringify(t, can_to_string<T>{});
}
所有丑陋的代码都隐藏在details
命名空间中。
如果你需要一个void_t
,使用这个:
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
它适用于大多数主要的 C++11 编译器。
请注意,更简单template<class...>using void_t=void;
的方法在一些较旧的 C++11 编译器中无法工作(标准中存在歧义)。