undefined

类模板和模板类

类模板

类模板:允许用户为类定义一种模式,使得类中的某些数据成员、默认成员函数的参数、某些成员函数的返回值,能够取任意类型(包括系统预定义和用户自定义)。如果一个类中数据成员的数据类型不能确定,或者是某个成员函数的参数或返回值的类型不能确定,就必须将此类声明为模板,它的存在不是代表一个具体的、实际的类,而是代表一类 类

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
class foo {
private:
T n;
const T i;
static T cnt;
public:
Test(): i(0) {}
Test(T k);
~Test(){}
void print();
T operator+(T x);
};
  1. 在类定义体外定义成员函数时,若此成员函数中有模板参数存在,则除了需要和一般类的体外定义成员函数一样的定义外,还需在函数体内进行模板声明

    1
    2
    3
    4
    template <class T> 
    void Test<T>::print() {
    ...
    }
  2. 在类定义体外初始化 const 成员和 static 成员变量的做法和普通类体外初始化 const 成员和 static 成员变量的做法基本上时一样的,唯一的区别是需再对模板进行声明

    1
    2
    template<class T> 
    int Test<T>::cnt = 0;

模板类

模板类其实就是 类模板实例化后的一个产物。我们把类模板比作一个做饼干的模子,而模板类就是用这个模子做出来的饼干

函数模板和模板函数

函数模板可以用来创建一个通用的函数,以支持多种不同的形参,避免重载函数的函数体重复设计。它的最大特点是把函数使用的数据类型作为参数。

模板函数的生成就是将函数模板的类型形参实例化的过程。

C++ template 的图灵完备

图灵完备:在可计算理论中,当一组数据操作的规则(一组指令集、编程语言,或者细胞自动机)满足任意数据按照一定的顺序可以计算出结果,被称为 图灵完备。

C++模板是图灵完全的,使用C++模板,可以在编译期间模拟一个完整的图灵机,也就是说,可以完成任何的计算任务

模板元编程(编译期计算)

1
2
3
4
5
6
7
8
9
10
11
12
13
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() {
std::cout << factorial<10>::value << std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <bool cond, 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 main() {
std::cout << typeid(If<true, int, double>::type).name() << std::endl; // int
}

使用模板来做一些高级的运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 integral_constant {
static const T value = v;
typedef T value_type;
typedef integral_constant type;
};

template <int result, int n>
struct SumLoop {
static const bool cond_value = n != 0;
static const int res_value = result;
typedef 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() {
std::cout << While<Sum<10>::type>::type::value << std::endl;
}

注意:使用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型。

编译期运行推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <type_traits>

typedef std::integral_constant<bool, true> true_type;
typedef std::integral_constant<bool, false> false_type;

template <typename T>
class SomeContainer {
public:
static void destroy(T* ptr) {
_destroy(ptr, std::is_trivially_destructible<T>());
}

private:
static void _destroy(T* ptr, true_type) {}
static void _destroy(T* ptr, false_type) {
ptr->~T();
}
};

很多容器类会有一个 destroy 函数,通过指针来析构某个对象。为了确保最大程度的优化,常用的技巧就是用 std::is_trivially_destructible 模板来判断类是否是可平凡析构的,也就是说,不调用析构函数,不会造成任何资源泄露问题。模板返回的结果还是一个类,要么是 true_type,要么是 false_type。如果要得到布尔值的话,当然使用 is_trivially_destructible::value 就可以,但此处不需要。我们需要的是,使用 () 调用该类型的构造函数,让编译器根据数值类型来选择合适的重载。这样,在优化编译的情况下,编译器可以把不需要的析构操作彻底全部删除。

类型转换用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <typeinfo>

template <class T>
struct remove_const {
typedef T type;
};

template <class T>
struct remove_const<const T> {
typedef T type;
};

int main() {
std::cout << typeid(remove_const<const char*>::type).name() << std::endl;
}

利用模板的特化,针对 const 类型去掉相应的属性。注意:const char* 应用于remove_const 的话,结果还是 const char*,原因是,const char* 是指向 const char 的指针,而不是指向 char 的const 指针。这个const 修饰的是指针指向的内容,而不是指针本身。如果对 char* const 应用 remove_const的话,还是可以得到 char*

函数模板的重载决议

模板之 SFINAE(替换失败非错(substation failure is not an error))

当一个函数名称和某个函数模板名称匹配时,重载决议过程大致如下:

  1. 根据名称找出所有适用的函数和函数模板
  2. 对于适用的函数模板,要根据实际情况对模板形参进行替换;替换过程中如果发生错误,这个模板会被丢弃
  3. 在上面两步生成的可行函数集合中,编译器会寻找一个最佳匹配,产生对该函数的调用
  4. 如果没有找到最佳匹配,或者找到多个匹配程度相当的函数,则编译器需要报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

struct Test {
typedef int foo;
};

template <typename T>
void f(typename T::foo) {
puts("1");
}

template <typename T>
void f(T) {
puts("2");
}

int main() {
f<Test>(10);
f<int>(10);
}
// 输出
1
2

分析:

1
2
3
4
5
6
7
8
对于 f<Test>(10); 的情况:
1. 我们有两个模板符合名字 f
2. 替换结果为 f(Test::foo) 和 f(Test)
3. 使用参数10去匹配,只有前者参数可以匹配,因而第一个模板被选择
对于 f<int>(10); 的情况:
1. 还是两个模板符合名字 f
2. 替换结果为 f(int::foo) 和 f(int); 显然前者不是个合法的类型,被抛弃
3. 使用参数 10 去匹配 f(int),没有问题,那就使用这个模板实例了

SFINAE 设计的最初用法:如果模板实例化中发生了失败,没有理由编译就此出错终止,因为还是可能有其他可用的函数重载的。这儿失败仅指函数模板的原型声明,即参数和返回值。函数体内的失败不考虑在内。如果重载决议选择了某个函数模板,而函数体在实例化的过程中出错,那我们仍然会得到一个编译错误。

关于 SFINAE 的其他还要看:https://time.geekbang.org/column/article/181636

可变模板实现编译期递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
constexpr auto sum(T x) {
return x;
}

template <typename T1, typename T2, typename... Targ>
constexpr auto sum(T1 x, T2 y, Targ... targ) {
return sum(x+y, targ...);
}

int main() {
auto result = sum(1,2,3,4,5,6,7,8,9,10);
std::cout << result << std::endl;
}

f189e2c8a73a1955b6dc08495a6b37c2b2b17d57