模板
模板特化
下面是求最大公因数的算法模板
template <typename T>
T gcd(T a, T b)
{
while(b != T(0)){
T t = a % b;
a = b;
b = t;
}
return a;
}
要求是:
- 可以通过常量 0 来构造
- 可以拷贝(构造和赋值)
- 可以作不等于的比较
- 可以进行取余数的操作
标准的 int、long、long long 等类型及其对应的无符号类型,以上代码都能正常工作,并能得到正确的结果。
这里有一个自建类型
class m_number{
public:
m_number() noexcept{}
m_number(size_t _val) noexcept:val(_val){}
m_number(const m_number& m){val = m.val;}
bool operator==(const m_number& m) noexcept
{
return val == m.val;
}
bool operator!=(const m_number& m) noexcept
{
return !(*this==m);
}
m_number& operator=(const m_number& m) noexcept
{
val = m.val;
return *this;
}
size_t operator*() const noexcept
{
return val;
}
private:
size_t val{0};
};
直接调用gcd(m_number(18), m_number(24);
会报错:
error: no match for 'operator%' (operand types are 'm_number' and 'm_number') T t = a % b;
1、添加 operator% 的实现在m_number内部:
m_number& operator%(const m_number& m) noexcept
{
val %= m.val;
return *this;
}
2、添加 operator% 的实现在外部:
m_number operator%(const m_number& m, const m_number& n)
{
return {*m % *n};
}
3、将gcd的a%b改为m_mod(a,b),并进行重载:
template <typename T>
T m_mod(const T& a, const T& b)
{
return a % b;
}
m_number m_mod(const m_number& a, const m_number& b)
{
return {*a % *b};
}
template <typename T>
T gcd(T a, T b)
{
while(b != T(0)){
T t = m_mod(a, b);
a = b;
b = t;
}
return a;
}
4、重载改为特化:
template <>
m_number m_mod<m_number>(const m_number& a, const m_number& b)
{
return {*a % *b};
}
特化和重载在行为上没有本质的区别。就一般而言,特化是一种更通用的技巧,最主要的原因是特化可以用在类模板和函数模板上,而重载只能用于函数。
通用而言,Herb Sutter 给出了明确的建议:对函数使用重载,对类模板进行特化。
动态多态和静态多态的对比
面向对象的动态多态,C++ 基于泛型编程的静态多态,两者解决的实际上是不太一样的问题。
动态多态解决的是运行时的行为变化,选择了一个形状后,再在某个地方绘制该形状,这是无法在编译时确定的。
静态多态或者泛型,让适用于不同类型的同构算法可以用同一套代码来实现,实际上强调的是对代码的复用。
以排序为例,C++ 里的标准 sort 算法(以两参数的重载为例)只要求:
- 参数满足随机访问迭代器的要求。
- 迭代器指向的对象之间可以使用 < 来比较大小,满足严格弱序关系。
- 迭代器指向的对象可以被移动。
它的性能超出 C 的 qsort,因为编译器可以内联(inline、可以看作C里面的宏定义)对象的比较操作;而在 C 里面比较只能通过一个额外的函数调用来实现。C 的 qsort 函数要求数组指向的内容是可按比特复制的,C++ 的 sort 则要求迭代器指向的内容是可移动的,可适用于更广的情况。
C++ 里目前有大量的泛型算法:
- sort:排序
- reverse:反转
- count:计数
- find:查找
- max:最大值
- min:最小值min
- max:最小值和最大值
- next_permutation:下一个排列
- gcd:最大公约数
- lcm:最小公倍数
编译期计算
C++ 模板是图灵完全的,使用 C++ 模板,你可以在编译期间模拟一个完整的图灵机,可以完成任何的计算任务。
// 先定义,再特化
template <int n>
struct factorial
{
static const int value = n * factorial<n - 1>::value;
};
template <>
struct factorial<0>
{
static const int value = 1;
};
int main()
{
cout << factorial<-1>::value<< endl;
return 0;
}
x86-64 下的编译结果:
.LC0:
.string "%d\n"
main:
push rbp
mov rbp, rsp
mov esi, 3628800
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
pop rbp
ret
传递一个负数给 factorial 应该是编译期间的递归溢出。
fatal error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum)
通用的解决方案是使用 static_assert
,确保参数永远不会是负数。
template <int n>
struct factorial
{
static_assert(n >= 0, "require non-negativ number!");
static const int value = n * factorial<n - 1>::value;
};
要进行编译期编程,最主要的一点,是需要把计算转变成类型推导。
条件语句:提供了两个特化。真的情况,定义结果 type 为 Then 分支;假的情况,定义结果 type 为 Else 分支。
template <bool _flag, typename Then, typename Else>
struct If;
template <typename Then, typename Else>
struct If<true, Then, Else>
{
typedef Then type;
};
template <typename Then, typename Else>
struct If<false, Then, Else>
{
typedef Else type;
};
int f(int i)
{
if (i & 1) return 0;
else return 1;
}
// m_integral_constant 模板同时包含了整数的类型和数值。
template <typename T, T v>
struct m_integral_constant {
static const T value = v;
typedef T value_type;
typedef m_integral_constant type;
};
template <int i>
struct F {
typedef typename If<i & 1, m_integral_constant<int, 0>, m_integral_constant<int, 1>>::type type;
};
int main()
{
cout << f(1) << endl; // 1
cout << F<1>::type::value << endl; // 1
return 0;
}
循环语句:WhileLoop有两个模板参数,同样用特化来决定走递归分支还是退出循环分支。While 模板则只需要循环体一个参数,方便使用。
用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型。上面循环模板的定义里就出现了多次这样的语法。MSVC 在这方面往往比较宽松,不写 typename 也不会报错,但这是不符合 C++ 标准的用法。
下面为如何使用循环模板来完成从 1 加到 n 的计算:
template <bool condition, typename Body>
struct WhileLoop;
template <typename Body>
struct WhileLoop<true, Body>
{
typedef typename WhileLoop<Body::cond_value, typename Body::next_type>::type type;
};
template <typename Body>
struct WhileLoop<false, Body>
{
typedef typename Body::res_type type;
};
template <typename Body>
struct While
{
typedef typename WhileLoop<Body::cond_value, Body>::type type;
};
template <class T, T v>
struct m_integral_constant {
static const T value = v;
typedef T value_type;
typedef m_integral_constant type;
};
template <int result, int n>
struct SumLoop {
// 代表循环的条件(真或假)
static const bool cond_value = n != 0;
static const int res_value = result;
// 退出循环时的状态
typedef m_integral_constant<int, res_value> res_type;
// 下面循环执行一次时的状态
typedef SumLoop<result + n, n - 1> next_type;
};
template <int n>
struct Sum {
typedef SumLoop<0, n> type;
};
int main()
{
cout << While<Sum<10>::type>::type::value << endl; //55
return 0;
}
编译期类型推导
C++ 标准库在<type_traits>
里定义了很多工具类模板,用来提取某个类型(type)在某方面的特点(trait)
针对布尔值有两个额外的类型定义:
typedef std::integral_constant<bool, true> true_type;
typedef std::integral_constant<bool, false> false_type;
true_type 和 false_type 经常可以在函数重载中见到。
template <typename T>
class SomeContainer {
public:
static void destroy(T* ptr)
{
_destroy(ptr, is_trivially_destructible<>());
}
private:
static void _destroy(T* ptr, true_type){}
static void _destroy(T* ptr, false_type){ptr->~T();}
};
很多容器类里会有一个 destroy 函数,通过指针来析构某个对象。为了确保最大程度的优化,常用的一个技巧就是用 is_trivially_destructible
模板来判断类是否是可平凡析构的,即不调用析构函数,不会造成任何资源泄漏问题。模板返回的结果还是一个类( true_type或false_type)。要得到布尔值使用 is_trivially_destructible::value 就可以,但此处需要的是使用 () 调用该类型的构造函数,让编译器根据数值类型来选择合适的重载。在优化编译的情况下,编译器可以把不需要的析构操作彻底全部删除。
类似的还有:
- is_array
- is_enum
- is_function
- is_pointer
- is_reference
- is_const
- has_virtual_destructor
一个常见的模板 remove_const 的定义如下:(用来去除类型里的 const 修饰)
template <class T>
struct remove_const
{
typedef T type;
};
template <class T>
struct remove_const<const T>
{
typedef T type;
};
利用模板的特化,针对 const 类型去掉相应的修饰。
对 const string& 应用 remove_const,就会得到 string&,即remove_const::type 等价于 string&。
如果对
const char*
应用 remove_const 的话,结果还是const char*
。原因是const char*
是指向 const char 的指针,而不是指向 char 的 const 指针。如果对char * const
应用 remove_const 的话,可以得到char*
。
is_trivially_destructible::value 和 remove_const::type 非常啰嗦,C++ 标准里,前者有增加 _v 的编译时常量,后者有增加 _t 的类型别名。
template <class T> inline constexpr bool is_trivially_destructible_v = is_trivially_destructible<T>::value; template <class T> using remove_const_t = typename remove_const<T>::type;
通用 fmap 函数模板
// default return container : vector
template <template <typename, typename>class OutContainer = std::vector, typename F, class R>
auto fmap(F&& fun, R&& input)
{
// decltype 来获得用 f 来调用 inputs 元素的类型
// decay_t 来把获得的类型变成一个普通的值类型
typedef std::decay_t<decltype(fun(*input.begin()))> res_type;
OutContainer<res_type, allocator<res_type>> res;
// 存放结果的容器需要支持 push_back 成员函数
// 这里是转发引用
for (auto&& item : input) {
res.push_back(fun(item));
}
return res;
}
int main()
{
vector<int> v{1,3,5};
auto res = fmap([](int a) {return 2 * a; }, v);
m_print_single(res); // 2 6 10
return 0;
}
SFINAE
替换失败非错(substitution failure is not an error),英文简称为 SFINAE。
当一个函数名称和某个函数模板名称匹配时,重载决议过程大致如下:
- 根据名称找出所有适用的函数和函数模板对于适用的函数模板,要根据实际情况对模板形参进行替换
- 替换过程中如果发生错误,这个模板会被丢弃
- 在上面两步生成的可行函数集合中,编译器会寻找一个最佳匹配,产生对该函数的调用
- 如果没有找到最佳匹配,或者找到多个匹配程度相当的函数,则编译器需要报错
struct Obj
{
typedef int obj_type;
};
template <typename T>
void f(typename T::obj_type)
{
cout << "typename T::obj_type" << endl;
}
template <typename T>
void f(T)
{
cout << "T" << endl;
}
int main()
{
f<Obj>(0); // typename T::obj_type
f<int>(0); // T
return 0;
}
对于f<Obj>(0);
,有两个模板符合名字 f,替换结果为 f(Obj::obj_type) 和 f(Obj),只有f(Obj::obj_type) 可以匹配参数0。
对于f<int>(0);
,有两个模板符合名字 f,替换结果为 f(int::obj_type) 和 f(int),int::obj_type不是正确的类型,只有f(int)可以匹配参数0。
SFINAE 设计的最初用法:如果模板实例化中发生了失败,没有理由编译就此出错终止,因为还是可能有其他可用的函数重载的。这儿的失败仅指函数模板的原型声明,即参数和返回值。
函数体内的失败不考虑在内。如果重载决议选择了某个函数模板,而函数体在实例化的过程中出错,那我们仍然会得到一个编译错误。
编译期成员检测
SFINAE可以根据某个实例化的成功或失败来在编译期检测类的特性。
template <typename T>
struct has_reserve {
// 定义了两个结构 good 和 bad,其大小必须不一样。
struct good { char _c; };
struct bad { char _c[2]; };
// 定义了一个 SFINAE 模板,内容不重要。
// 模板的第二个参数需要是第一个参数的成员函数指针,并且参数类型是 size_t,返回值是 void。
template <class V, void (V::*)(size_t)>
struct SFINAE {};
// 定义了一个要求 SFINAE* 类型的 reserve 成员函数模板,返回值是 good。
template <class V>
static good reserve(SFINAE<V, &V::reserve>*);
// 定义了一个对参数类型无要求的 reserve 成员函数模板,返回值是 bad。
template <class V>
static bad reserve(...);
// 定义常整型布尔值 value,结果是 true 还是 false, 取决于 nullptr 能不能和 SFINAE* 匹配成功。
// 这又取决于模板参数 T 有没有返回类型是 void、接受一个参数并且类型为 size_t 的成员函数 reserve。
static const bool value = sizeof(reserve<T>(nullptr)) == sizeof(good);
};
int main()
{
cout << has_reserve<vector<int>>::value << endl; // 1
cout << has_reserve<Obj>::value << endl; // 0
return 0;
}
C++11 开始,标准库有了一个 enable_if 的模板(定义在<type_traits>
),可用它来选择性地启用某函数重载。
设我们有一个函数,用来往一个容器尾部追加元素。容器有没有 reserve 成员函数,是对性能有影响的。
如果有的话,我们通常应该预留好内存空间,以免产生不必要的对象移动甚至拷贝操作。
template <typename C, typename T>
enable_if_t<has_reserve<C>::value, void>
append(C& container, T* ptr, size_t size)
{
container.reserve(container.size() + size);
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
template <typename C, typename T>
enable_if_t<!has_reserve<C>::value,void>
append(C& container, T* ptr, size_t size)
{
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
对于某个 type trait,添加 _t 的后缀等价于其 type 成员类型。因此可以用 enable_if_t 来取到结果的类型。
enable_if_t<has_reserve<C>::value, void>
指如果类型 C 有 reserve 成员的话,那启用下面的成员函数,返回类型为 void。
decltype 返回值
如果只需要在某个操作有效的情况下启用某个函数,而不需要考虑相反的情况的话,有另外一个技巧可以用。
如果我们想限制只有具有 reserve 成员函数的类可以使用这个重载,可以把代码简化成:
template <typename C, typename T>
auto append(C& container, T* ptr, size_t size)->decltype(declval<C&>().reserve(1U), void()
{
container.reserve(container.size() + size);
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
declval 这个模板用来声明一个某个类型的参数,但这个参数只是用来参加模板的匹配,不允许实际使用。使用这个模板,我们可以在某类型没有默认构造函数的情况下,假想出一个该类的对象来进行类型推导。
declval<C&>().reserve(1U)用来测试 C& 类型的对象是不是可以拿 1U 作为参数来调用 reserve 成员函数。
C++ 里的逗号表达式的意思是按顺序逐个估值,并返回最后一项。上面这个函数的返回值类型是 void。
void_t
void_t 是 C++17 新引入的一个模板,定义如下:
template <typename...>
using void_t = void;
这个类型模板会把任意类型映射到 void。在这个过程中,编译器会检查任意类型的有效性。
利用 decltype、declval 和模板特化,可以简化 has_reserve 的定义。
template <typename T, typename = void_t<>>
struct has_reserve : false_type {};
template <typename T>
struct has_reserve<T, void_t<decltype(declval<T&>().reserve(1U))>> : true_type {};
int main()
{
cout << has_reserve<vector<int>>::value << endl; // 1
cout << has_reserve<Obj>::value << endl; // 0
return 0;
}
这里第二个 has_reserve 模板的定义实际上是一个偏特化。偏特化是类模板的特有功能,跟函数重载有些相似。编译器会找出所有的可用模板,然后选择其中最特别的一个。
像上面的例子,所有类型都能满足第一个模板,但不是所有的类型都能满足第二个模板,所以第二个更特别。当第二个模板能被满足时,编译器就会选择第二个特化的模板;而只有第二个模板不能被满足时,才会回到第一个模板的通用情况
标签分发
用 true_type 和 false_type 来选择合适的重载,即标签分发(tag dispatch)。
template <typename T>
struct has_reserve {
// 定义了两个结构 good 和 bad,其大小必须不一样。
struct good { char _c; };
struct bad { char _c[2]; };
// 定义了一个 SFINAE 模板,内容也同样不重要。
// 模板的第二个参数需要是第一个参数的成员函数指针,并且参数类型是 size_t,返回值是 void。
template <class V, void (V::*)(size_t)>
struct SFINAE {};
// 定义了一个要求 SFINAE* 类型的 reserve 成员函数模板,返回值是 good。
template <class V>
static good reserve(SFINAE<V, &V::reserve>*);
// 定义了一个对参数类型无要求的 reserve 成员函数模板,返回值是 bad。
template <class V>
static bad reserve(...);
// 定义常整型布尔值 value,结果是 true 还是 false, 取决于 nullptr 能不能和 SFINAE* 匹配成功。
// 而这又取决于模板参数 T 有没有返回类型是 void、接受一个参数并且类型为 size_t 的成员函数 reserve。
static const bool value = sizeof(reserve<T>(nullptr)) == sizeof(good);
};
template <typename C, typename T>
void _append(C& container, T* ptr, size_t size, true_type)
{
container.reserve(container.size() + size);
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
template <typename C, typename T>
void _append(C& container, T* ptr, size_t size, false_type)
{
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
_append(container, ptr, size, integral_constant<bool, has_reserve::value>{});
}
如果用void_t会简单一点
template <typename T, typename = void_t<>>
struct has_reserve : false_type {};
template <typename T>
struct has_reserve<T, void_t<decltype(declval<T&>().
template <typename C, typename T>
void _append(C& container, T* ptr, size_t size, true
{
container.reserve(container.size() + size);
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
template <typename C, typename T>
void _append(C& container, T* ptr, size_t size, fals
{
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
_append(container, ptr, size, has_reserve<C>{});
}
静态多态的限制
下面的代码的问题
template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
if (has_reserve<C>::value) {
container.reserve(container.size() + size);
}
for (size_t i = 0; i < size; ++i) {
container.push_back(ptr[i]);
}
}
C 类型没有 reserve 成员函数的情况下,编译是不能通过的,会报错。这是因为 C++ 是静态类型的语言,所有的函数、名字必须在编译时被成功解析、确定。在动态类型的语言里,只要语法没问题,缺成员函数要执行到那一行上才会被发现。这赋予了动态类型语言相当大的灵活性;只不过,不能在编译时检查错误,同样也是很多人对动态类型语言的抱怨所在。
可以用if constexpr代替if实现。因为一般的 if 是运行期条件语句;if constexpr 是编译期条件语句。
- 本文链接:https://morisa66.github.io/2021/03/03/Template/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。