从模板特化开始

考虑下面的代码

template<typename T>
bool equals(const T& a, const T& b) {
    return a == b;
}

void test() {
    printf("%d, %d", equals(1, 1), equals(0.42f - 0.4f, 0.02f));
}

运行这段代码,很容易得到结果:1, 0
这是因为浮点数计算不精确导致的,因此我们需要一个特化的 equals

template<typename T>
bool equals(const T& a, const T& b) {
    return a == b;
}

template<>
bool equals(const float& a, const float& b) {
    return std::fabs(a - b) <= 1e-6;
}

再次运行代码,得到的是正确的结果:1, 1
现在做一些小更改:把 test 中的 f 去掉,结果又不对了。因为我们只特化了 float,没有特化 double
这样下来,除了 double,还有 long double,甚至还会有自定义的各种浮点类型,如果对它们每一个都写一遍特化,这未免过于重复麻烦:要是所有浮点类型都使用同一段代码判断,那就好了。

两种方案

使用 STL 自带的模板特化工具,需要包含头文件 <type_traits>

传统方案:Tag dispatch

从 C++11 开始,STL 中提供了编译期布尔类型 std::true_typestd::false_type。配合类型选择模板 std::conditional 可在不同类型之间进行选择。
std::conditional 有3个模板参数,第一个参数 Cond 是一个编译期布尔值,在值为1和0时分别选择第2、3个参数类型作为 ::value 的类型。从 C++14 开始,可使用 std::conditional_t<...> 代替 typename std::conditional<...>::value,更加简洁。然后,对得到的 conditional 类型进行实例化,得到对应 Tag 类型的一个对象,从而调用对应的模板。参考以下代码:

template<typename T>
bool equals(const T& a, const T& b, std::true_type) {
    return std::fabs(a - b) <= 1e-6;
}

template<typename T>
bool equals(const T& a, const T& b, std::false_type) {
    return a == b;
}

template<typename T>
inline bool equals(const T& a, const T& b) {
    return equals(a, b, std::conditional_t<std::is_floating_point_v<T>, std::true_type, std::false_type>{});
}

std::is_floating_point<T>T 为浮点类型时将布尔型成员 value 的值设为 true,否则为 false。从 C++14 开始,可使用 std::is_floating_point_v<T> 代替 std::is_floating_point<T>::value

SFINAE

SFINAE 全名为 Substitution failure is not an error:匹配失败不是错误。即模板匹配失败时不是一种错误,而是会继续匹配下去直到找到正确的模板。

std::enable_if

clang 库中 std::enable_if 定义如下:

// Define a member typedef type only if a boolean constant is true.
template<bool, typename _Tp = void>
struct enable_if{};

// Partial specialization for true.
template<typename _Tp>
struct enable_if<true, _Tp>{
    typedef _Tp type;
};

std::enable_if<bool, typename _Tp = void> 只有在第一个模板参数为 true 时,::type 才会有定义。因此当第一个模板参数为 false 时,将不存在 ::type,进而编译器在找不到这样一个定义时会继续寻找下一个存在这样定义的位置。从 C++14 开始,可使用 std::enable_if_t<...> 代替 typename std::enable_if<...>::type
std::enable_if 使用样例如下:

template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
bool equals(const T& a, const T& b) {
    return std::fabs(a - b) <= 1e-6;
}

在这种情况下,equals(0.42 - 0.4, 0.02) 将会被正确选择,而 equals(1, 1) 将会因找不到对应模板而报错。
不过,下面的写法是错误的,会发生 Redefinition:

template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
bool equals(const T& a, const T& b) {
    return std::fabs(a - b) <= 1e-6;
}

template<typename T, typename = std::enable_if_t<!std::is_floating_point_v<T>>>
bool equals(const T& a, const T& b) {
    return a == b;
}

这是因为模板是“元编程”,两个生成的 equals 将具有相同的签名。
解决方法有两种,一是设置第三个不同类型的参数。还记得前面 _Tp 默认的类型 void 吗?我们可以将第三个参数设为空的 void 指针,从而使函数具有不同签名:

template<typename T>
bool equals(const T& a, const T& b, typename std::enable_if_t<std::is_floating_point_v<T>>* = nullptr) {
    return std::fabs(a - b) <= 1e-6;
}

template<typename T>
bool equals(const T& a, const T& b, typename std::enable_if_t<!std::is_floating_point_v<T>>* = nullptr) {
    return a == b;
}

第二种解决方案是在返回值上动手脚:

template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, bool> equals(const T& a, const T& b) {
    return std::fabs(a - b) <= 1e-6;
}

template<typename T>
std::enable_if_t<!std::is_floating_point_v<T>, bool> equals(const T& a, const T& b) {
    return a == b;
}

至于哪种写法更优雅,这就见仁见智了。

最后修改:2021 年 11 月 30 日
如果觉得我的文章对你有用,请随意赞赏