模板函数偏特化的实现方法

2023/05/07 C++ 共 3168 字,约 10 分钟

前言

C++函数模板并不支持偏特化,但可以用一些方法去变通,这篇文章主要讨论一些主流的函数偏特化的实现方法。

1. C++函数模板

C++函数模板不支持偏特化,因为偏特化和函数重载容易出现歧义,下面的讨论假设支持偏特化。比如有下面2个函数:

// 普通函数
template <typename T>
T add(T a, T b) {
    return a + b;
}
// 偏特化函数,特化出指针版本
template <typename T*>
T* add(T* a, T* b) {
    return *a + *b;
}

有以下的测试代码,

int x = 5, y = 10;
int* px = &x;
int* py = &y;
std::cout << add(x, y) << std::endl;   // 调用普通的函数模板
std::cout << add(px, py) << std::endl; // 会导致歧义

其中第二个调用会导致歧义,因为 px 是指针,第二个模板的 T 被推导成 int,但同时第一个模板的 T 被推导为 int *,依然同样的匹配。出现歧义,结构体(类模板)偏特化不会引起歧义。这是因为在类模板中,每个特化版本对应着一个不同的类,编译器可以根据需要选择适当的特化版本。

虽然函数模板不支持偏特化,但是函数模板支持全特化,全特化和普通函数一样,在头文件中定义需要内联。

2. 如何实现偏特化

2.1 类函数

首先最容易想到的例子就是类函数,把函数写成类函数的形式就可以利用类的偏特化间接实现函数的偏特化了。

2.2 标签派发

使用标签分发(Tag Dispatch)的方案就是通过函数实现不同的函数重载实现,根据不同实参类型选择具体的函数实现,以达到函数模板偏特化的实现。比如下面这个例子。首先利用类的偏特化进行标签的分发然后利用标签选择不同的函数(重载进行选择)

#include <iostream>
// 标签
struct IntTag{};
struct NonIntTag{};
// 利用类的偏特化分发标签
template <typename T>
struct Dispatch {
  using Tag = NonIntTag;
};
template <>
struct Dispatch<int> {
  using Tag = IntTag;
};

// 利用重载实现
template <typename T>
inline void func_impl(T t, IntTag) {
  std::cout << "operation on integral type: " << t << std::endl;
}
template <typename T>
inline void func_impl(T t, NonIntTag) {
  std::cout << "operation on non-integral type: " << t << std::endl;
}

template <typename T>
void func(T t) {
  func_impl(t, typename Dispatch<T>::Tag {});
}
int main() {
    func(42);     // 整数类型
    func(3.14);   // 非整数类型
    return 0;
}

2.3 SFINA

上面的实现还是略显复杂,SFINASubstitution Failure Is Not An Error)为我们带来了更加简洁的代码。

C++20以前,SFINA一般都是使用enbale_if实现,其定义如下,如果B为true,则 std::enable_if 拥有等同于 T 的公开成员 typedef type ;否则,无该成员 typedef 。即是说如果 B为true,std::enable_if 里面有个type类型为T,否则没有。

#include<type_traits> 
template< bool B, class T = void >
struct enable_if; 

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

使用SFINA的代码如下,其中使用 enable_if_t代替 typename enable_if<B,T>::type可以精简代码。

#include <iostream>
#include <type_traits>

//  假如是整数, std::enable_if_t<std::is_integral_v<T>, int> 被推导为 std::enable_if_t<true, int>
//  std::enable_if_t 那么 等价于 std::enable_if::<true, int>::type ,最终推导为 int,
//  如果为false,那么 std::enable_if::<true, int>::type 里面没有type这个类型,
//  推导失败,重载决议的时候会放弃这个函数,最终达到了偏特化的目标
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> 被推导为  = 0>
void func(T t) {
    std::cout << "operation on integral type: " << t << std::endl;
}

template <typename T, std::enable_if_t<!std::is_integral_v<T>, int> = 0>
void func(T t) {
    std::cout << "operation on non-integral type: " << t << std::endl;
}

int main() {
    func(42);     // 整数类型
    func(3.14);   // 非整数类型

    return 0;
}

上面的enable_ifC++20以前非常的常见,但是直到C++20,模板编程利器concept的出现让一切都简单灵活了起来。如下代码

#include <iostream>
#include <type_traits>

template <typename T>
concept IntegralType = std::is_integral_v<T>;

template <typename T>
void func(T t) requires IntegralType<T> {
    std::cout << "operation on integral type: " << t << std::endl;
}

template <typename T>
void func(T t) requires (!IntegralType<T>) {
    std::cout << "operation on non-integral type: " << t << std::endl;
}

int main() {
    func(42);     // 整数类型
    func(3.14);   // 非整数类型
    return 0;
}

require类型和普通泛化模板同时匹配,优先匹配require类型

conclusion

C++模板的偏特化已经是有非常成熟的做法了,在C++20以前,使用enable_if,在这之后,使用concept。都可以很好的解决我们的需求。

Search

    Table of Contents