首先,我认为SFINAE通常应该隐藏在接口之后。它会使接口变得混乱。将SFINAE放在表面之外,并使用标记分派来选择重载。
其次,我甚至将SFINAE从traits类中隐藏起来。根据我的经验,在写“我是否能做X”代码时很常见,我不想写复杂的SFINAE代码来实现它。因此,我编写了一个通用的can_apply trait,并使用decltype定义了一个在传递了错误类型时SFINAE失败的trait。
然后,我们将SFINAE失败的decltype trait传递给can_apply,并根据应用程序是否失败获取一个true/false类型。
这样可以将每个“我是否能做X” trait的工作量降至最小,并将有些棘手和脆弱的SFINAE代码远离日常工作。
我使用C++1z的void_t。自己实现它很容易(在本答案的底部)。
类似于can_apply的元函数正在C++1z中提出标准化,但它还没有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{};
}
我们可以根据
details::can_apply
来编写
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>
,当我们可以对T
进行to_string
时,它为true。
现在编写类似的新特性只需要2-4行简单的代码 - 只需创建一个decltype
using
别名,然后对其进行can_apply
测试即可。
一旦我们拥有了这个特性,就可以使用标签分派到适当的实现:
template<typename T>
std::string stringify(T t, std::true_type ){
return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type ){
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编译器中无法正常工作(标准中存在歧义)。
std::void_t
或者std::__void_t
替换void_t
,结果是一样的。 - Oktalistvoid_t
的好处是它能够正常工作。Matthis Vega的回答无法正常工作。 - Barry