和C++函数一样,类也可以把一个或多个类型参数化,标准库中的容器便是典型的例子,下面便是一个模板类的定义:
#include <cstdio>
#include <cstring>
#include <cassert>
template <typename T>
class Array final
{
public:
Array(void) : m_Data(nullptr), m_Size(0) {}
Array(const T *data, size_t size) : m_Data(nullptr), m_Size(0) {
if (data != nullptr && size > 0) {
m_Size = size;
m_Data = new T[m_Size];
memcpy(m_Data, data, sizeof(T) * m_Size);
}
}
Array(const Array &);
Array(Array &&);
Array &operator=(const Array &);
Array &operator=(Array &&);
Array &Swap(Array &);
const T *operator()(void) {
return m_Data;
}
T operator[](size_t i) {
assert(m_Data != nullptr && i < m_Size);
return m_Data[i];
}
private:
T *m_Data;
size_t m_Size;
};
template <typename T>
Array<T>::Array(const Array &other)
: m_Data(nullptr)
, m_Size(0)
{
if (other.m_Data != nullptr && other.m_Size != 0) {
m_Size = other.m_Size;
m_Data = new T[m_Size];
memcpy(m_Data, other.m_Data, sizeof(T) * m_Size);
}
}
template <typename T>
Array<T>::Array(Array &&other)
{
m_Data = other.m_Data;
other.m_Data = nullptr;
m_Size = other.Size;
other.Size = 0;
}
template <typename T>
Array<T> &Array<T>::operator=(const Array &rhs)
{
if (&rhs == this)
return *this;
if (m_Data != nullptr)
delete[] m_Data;
if (rhs.m_Data != nullptr && rhs.m_Size != 0) {
m_Size = rhs.m_Size;
m_Data = new T[m_Size];
memcpy(m_Data, rhs.m_Data, sizeof(T) * m_Size);
}
return *this;
}
template <typename T>
Array<T> &Array<T>::operator=(Array &&rhs)
{
if (&rhs == this)
return *this;
if (m_Data != nullptr)
delete[] m_Data;
m_Data = rhs.m_Data;
rhs.m_Data = nullptr;
m_Size = rhs.Size;
rhs.Size = 0;
return *this;
}
template <typename T>
Array<T> &Array<T>::Swap(Array &other)
{
auto data = other.m_Data;
auto size = other.m_Size;
other.m_Data = m_Data;
other.m_Size = m_Size;
m_Data = data;
m_Size = size;
return *this;
}
int main(int argc, char **argv)
{
char buf[] = "0123456789";
size_t len = strlen(buf);
Array<char> str(buf, len);
Array<char> str0;
str0.Swap(str);
for (size_t i = 0; i < len; ++i)
printf("%c", str0[i]);
printf("\n\n");
return 0;
}
1 类模板的定义
同C++普通类一样,类模板的成员函数的实现即可以放到类的内部,也可以放到类的外部。这里需要注意两个细节,每个成员函数的实现都必须使用‘template <typename T>’来表示这是一个模板,并且::(域操作符)之前使用Array<T>取代Array,来限制使用的类型。
如果仔细观察,会发现成员函数的赋值函数的返回值类型必须使用Array<T>,而形参的类型却可以直接使用Array。因为在模板类的内部(包括类的定义,成员函数形参,成员函数体)使用Array等同于Array<T>。
2 类模板的使用
类模板成员函数只有在被调用时才会被实例化。直接使用文章开始的源码编译生成二进制程序,使用‘objdump --syms array’观察,得到如下的结果:
SYMBOL TABLE:
0000000100003cd4 w F __TEXT,__text .hidden __ZN5ArrayIcEC1EPKcm
0000000100003d10 w F __TEXT,__text .hidden __ZN5ArrayIcEC1Ev
0000000100003e34 w F __TEXT,__text .hidden __ZN5ArrayIcEC2EPKcm
0000000100003ed4 w F __TEXT,__text .hidden __ZN5ArrayIcEC2Ev
0000000100003d3c w F __TEXT,__text __ZN5ArrayIcE4SwapERS0_
0000000100003d94 w F __TEXT,__text __ZN5ArrayIcEixEm
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100003bbc g F __TEXT,__text _main
0000000000000000 *UND* __Znam
0000000000000000 *UND* ___assert_rtn
0000000000000000 *UND* ___stack_chk_fail
0000000000000000 *UND* ___stack_chk_guard
0000000000000000 *UND* _memcpy
0000000000000000 *UND* _printf
0000000000000000 *UND* _strlen
而注释掉Swap的调用,如下:
int main(int argc, char **argv)
{
char buf[] = "0123456789";
size_t len = strlen(buf);
Array<char> str(buf, len);
/*Array<char> str0;
str0.Swap(str);
for (size_t i = 0; i < len; ++i)
printf("%c", str0[i]);
printf("\n\n");*/
return 0;
}
再次使用‘objdump --syms array’观察生成的二进制程序,结果如下:
SYMBOL TABLE:
0000000100003e90 w F __TEXT,__text .hidden __ZN5ArrayIcEC1EPKcm
0000000100003ecc w F __TEXT,__text .hidden __ZN5ArrayIcEC2EPKcm
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100003dfc g F __TEXT,__text _main
0000000000000000 *UND* __Znam
0000000000000000 *UND* ___stack_chk_fail
0000000000000000 *UND* ___stack_chk_guard
0000000000000000 *UND* _memcpy
0000000000000000 *UND* _strlen
仔细观察,会发现swap函数没有被实例化。
类模板存在静态成员,该静态成员仅需被实例化一次(未完理解这段的含义)。如下:
template <typename T>
class MyType final
{
public:
static std::string m_TypeName;
static T m_Elem;
};
template <typename T>
std::string MyType<T>::m_TypeName = "unknow";
template <typename T>
T MyType<T>::m_Elem = T();
3 类型别名
在c++11之前,仅能通过typedef定义类型别名,如下:
typedef std::vector<int> int_vec;
c++11开始,又多了一种定义新类型的方式——通过using关键字定义类型别名,如下:
using int_vec = std::vector<int>;
4 类模板的类型推导
从c++17开始,不用指定模板类型参数,就可以调用类的拷贝构造函数和赋值函数,如下:
std::vector<int> vec0 {1, 2, 3, 4, 5};
std::vector vec1 = vec0;
std::vector vec2(vec0);
虽然上面这种初始化方式很方便,但个人还是觉得auto更简洁一些:
auto vec3 = vec0;
如果模板类有一个构造函数仅需要一个参数,并且参数的类型为模板参数,那么可以通过下面的方式直接推导出模板类实例的类型,完成对象的初始化:
template <typename T>
class MyPtr
{
public:
MyPtr(T data) : m_DataPtr(new T(data)) {}
virtual ~MyPtr(void) {
if (m_DataPtr != nullptr) {
delete m_DataPtr;
m_DataPtr = nullptr;
}
}
T *operator->(void) {
return m_DataPtr;
}
protected:
T *m_DataPtr;
};
......
MyPtr pi = 1;
其实,上面这种方式和隐式类型转换异曲同工,想想std::string str = "hello"。
5 聚合类的模板化
聚合类的定义
- 没有构造函数(包括用户显示定义的构造函数,从基类继承而来的构造函数)
- 没有private或protected修饰的成员变量(包括从基类继承而来的成员变量)
- 没有虚函数(包括从基类继承而来的虚函数)
下面struct ValueWithComment便是一个聚合类:
template <typename T>
struct ValueWithComment {
T value;
std::string comment;
};
使用ValueWithComment定义并初始化一个对象,传统方式如下:
ValueWithComment<int> vc1{10, "ten"};
ValueWithComment<const char *> vc2 = {"10", "ten"};
当c++17引入类型推断指引后,定义并初始化一个对象变的更加简洁,如下:
ValueWithComment(int, const char *) -> ValueWithComment<int>;
ValueWithComment(char const*, char const*) -> ValueWithComment<std::string>;
......
ValueWithComment vc1{10, "ten"};
ValueWithComment vc2 = {"10", "ten"};
当然,通过传统方式,使用typedef或define也能够实现类似效果,但看上去远远没有类型推断指引优雅,如下:
#define ValueWithComment_int ValueWithComment<int>
typedef ValueWithComment<std::string> ValueWithComment_string;
......
ValueWithComment_int vc1{10, "ten"};
ValueWithComment_string vc2 = {"10", "ten"};
使用类型推断指引类型一定要严格遵守聚合类规范,否则编译出错。