C++模板特化、类型萃取及类型限定学习笔记
1.为什么要有模板特化?
前面说到,实现一个通用的功能,如,Add
函数,需要穷举类型组合,重复实现相同算法,因此,引入模板将数据类型(变化部分)从代码实现(不变部分)中剥离,进而大大地减少了重复代码地编写。
但是当前模板实现可能无法适配一些特殊情况,如:
#include <iostream>
#include <cmath>
struct Point2f {
float x;
float y;
};
template <typename T>
float Norm(const T value) {
return fabs(value);
}
int main(int argc, char *argv[]) {
float a = 5.0f;
std::cout << Norm(a) << std::endl;
Point2f pt {1.0f, 2.0f};
std::cout << Norm(pt) << std::endl; // 错误
return 0;
}
显然代码中 Norm
函数对于自定义数据结构 Point2f
不再适用,为了适配这种自定义数据结构,需要引入特定实现,这也就是模板特化:
#include <iostream>
#include <cmath>
struct Point2f {
float x;
float y;
};
template <typename T>
float Norm(const T value) {
return fabs(value);
}
template <>
float Norm<Point2f>(const Point2f value) {
return sqrtf(value.x * value.x + value.y * value.y);
}
int main(int argc, char *argv[]) {
float a = 5.0f;
std::cout << Norm(a) << std::endl;
Point2f pt {1.0f, 2.0f};
std::cout << Norm(pt) << std::endl;
return 0;
}
归根到底,模板特化实际上就是为了处理一些模板通用实现无法处理的 corner case
,进而让当前模板能够更加通用。
2.模板全特化和模板偏特化
模板特化又分为全特化和偏特化,什么叫特化呢?其实你可以理解为将模板参数直接指定成特定类型或限定到特定范围,全特化就是当前模板实现中将所有模板类型都指定成特定类型,而偏特化则是当前模板实现中只将部分模板类型指定为特定类型或特定范围。举例说明一下:
#include <iostream>
#include <string>
template <typename T1, typename T2>
void func(T1 x, T2 y) {
std::cout << "Normal template." << std::endl;
}
// 函数模板不允许部分偏特化,所以下面的代码无法通过编译
//template <typename T1>
//void func<T1, int>(T1 x, int y) {
// std::cout << "Partial speicial template." << std::endl;
//}
template <>
void func<std::string, std::string>(std::string a, std::string b) {
std::cout << "Speicial template." << std::endl;
}
int main(int argc, char *argv[]) {
std::string a = "Hello";
std::string b = "world";
int c = 30;
func(a, b); // 输出 "Speicial template."
func(a, c); // 输出 "Normal template."
return 0;
}
这里注意,函数模板不允许部分偏特化,类模板才允许部分偏特化,如:
#include <iostream>
#include <string>
template <typename T1, typename T2>
class A {
public:
void func(T1 x, T2 y) {
std::cout << "Normal template." << std::endl;
}
};
template <typename T1>
class A <T1, int> {
public:
void func(T1 x, int y) {
std::cout << "Partial special template." << std::endl;
}
};
template <>
class A <std::string, std::string> {
public:
void func(std::string x, std::string y) {
std::cout << "Speicial template." << std::endl;
}
};
int main(int argc, char *argv[]) {
A<int, double> a;
a.func(1, 2.0); // Normal template.
A<std::string, int> b;
b.func("Hello", 1); // Partial special template.
A<std::string, std::string> c;
c.func("Hello", "World"); // Speicial template.
return 0;
}
3.模板传入类型地类型萃取
模板特化主要用于处理通用实现无法适配的特殊情况,进而提升模板的通用性。但是为了优化代码效率,有时就需要根据传入模板参数类型来选择不同传参方式(值、引用或指针)。
例如,对于一个函数接口,我们期望传参类型为基本内置类型(如,float,double,int,char...)时,传参方式为值传递方式,当传参类型为指针类型时,传参方式为传指针方式,传参类型为其它类型时,传参方式则为传引用方式,这时,就需要类型萃取:
#include <iostream>
#include <string>
// 通用模板
template <typename T>
struct ArgType {
using type = const T &;
};
// 偏特化模板,处理指针传入
template <typename T>
struct ArgType<T *> {
using type = T *;
};
// 全特化模板,处理基本内置类型
template <>
struct ArgType<int> {
using type = int;
};
template <>
struct ArgType<double> {
using type = double;
};
template <>
struct ArgType<float> {
using type = float;
};
template <>
struct ArgType<char> {
using type = char;
};
// 模板类型定义
template <typename T>
using ArgType_t = typename ArgType<T>::type;
template <typename T>
void Show(ArgType_t<T> t) {
std::cout << typeid(t).name() << std::endl;
}
int main(int argc, char *argv[]) {
int a {0};
std::string b;
char c = 1;
double d {0.0};
char *p = nullptr;
Show<int>(a);
Show<std::string>(b);
Show<char>(c);
Show<double>(d);
Show<char *>(p);
return 0;
}
输出结果:
int
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
char
double
char * __ptr64
这里使用了 typeid()
函数来获取当前传入参数类型信息,使用 .name()
方法来打印对应名称,可以看到,通过类型萃取的方式,我们更加精细的控制了模板调用行为,进而来优化了代码运行效率。
4.模板传入参数类型限定
对于更复杂地控制,例如,判断模板函数的传入类型是否实现方法 method
,如果没有实现,就不能调用当前函数:
4.1 条件判断
首先需要判断传入模板参数是否包含 method
方法,并获取返回值类型:
- (1) 判断传入参数类型是否有
method
方法:这种方法默认传入类型需要有无参构造函数
// 1.构造一个T的实例,然后调用method方法
decltype(T{}.method())
- (2) 将
nullptr
转换成T*
类型,再调用method()
方法:这种方法太过于臃肿
// 2.将nullptr转换成T*类型,然后调用method方法
decltype(static_cast<T*>(nullptr)->method())
- (3) 使用
std::delval()
假定一个T类型对象,然后调用method
方法:
// 3.使用std::declval假定一个T类型对象,然后调用method方法
decltype(std::declval<T>().method())
然后,需要判断返回值类型是否为 void
类型,这里就需要用到 std::is_same_v
:
std::is_same_v<decltype(std::declval<T>().method()), void>
这里还是有一个问题,这样判断还是判断的结果是:当前传入模板类型要包含 method
方法,并且返回类型要为 void
。若我们仅仅只需要判断是否包含 method
方法呢?
这里可以使用 std::void_t
将返回类型直接都转换 void
类型,再判断是否返回类型为 void
,曲线救国一下,就等效于判断当前传入模板类型是否包含 method
方法:
std::is_same_v<std::void_t<decltype(std::declval<T>().method())>, void>
4.2 条件控制
这里需要用到 std::enable_if_t
来根据前面条件判断结果来决定是否启用当前模板实现:
#include <iostream>
#include <type_traits>
struct Test1 {
void method() {
std::cout << "Test1::method()" << std::endl;
}
};
struct Test2 {
int method() {
std::cout << "Test2::method()" << std::endl;
return 0;
}
};
struct Test3 {
};
//template <typename T>
//std::enable_if_t<std::is_same_v<std::void_t<decltype(std::declval<T>().method())>, void>, void>
//callMethod(T t) {
// t.method();
//}
template <typename T>
std::enable_if_t<std::is_void_v<std::void_t<decltype(std::declval<T>().method())> >, void>
callMethod(T t) {
t.method();
}
int main(int argc, char *argv[]) {
Test1 t1;
callMethod<Test1>(t1);
Test2 t2;
callMethod<Test2>(t2);
Test3 t3;
// callMethod<Test3>(t3); // This will cause a compile error
return 0;
}
输出结果为:
Test1::method()
Test2::method()
C++的 type_traits
头文件中包含了所有关于类型萃取及条件控制相关代码。