类模板
我们采用T类型的元素数组来介绍类模板,以下是这个类至少应该包括的内容:
//全在MyClasses.hpp,因为模板不能分文件编译
template <typename T>
class Array
{
public:
explicit Array(size_t size); //构造函数,explicit防止隐式转换
~Array();
Array(const Array& array); //拷贝构造函数
Array& operator=(const Array& rhs); //复制赋值函数
T& operator[](size_t size); //重载[]运算符
const T& operator[](size_t size) const; //同样是重载[]运算符,但是是const版本
size_t getSize() const { return size;} //获得Array数组的大小
void swap(Array& other) noexcept; //通过先复制后交换的操作来实现强异常安全
private:
size_t m_size; //记录数组大小
T* m_elements; //使用指针来指向创建T类型数组的返回值
};
类模板的参数不一定只有一个或者可变的类型,其声明可以如下:
template <typename T>
class Array
{
public:
explicit Array(size_t size);
~Array();
Array(const Array& array);
Array& operator=(const Array& rhs);
T& operator[](size_t size);
const T& operator[](size_t size) const;
size_t getSize() const { return m_size; }
void swap(Array& other) noexcept;
private:
size_t m_size;
T* m_elements;
};
template<typename T>
Array<T>::Array(size_t size)
:m_size{ size }, m_elements{ new T[size] {} }
{}
template<typename T>
Array<T>::~Array()
{
delete[] m_elements;
}
template<typename T>
Array<T>::Array(const Array& array)
:Array{ array.m_size }
{
for (int i = 0; i < m_size; ++i)
m_elements[i] = array.m_elements[i];
}
//template<typename T>
//T& Array<T>::operator[](size_t index)
//{
// if (index >= m_size)
// throw std::out_of_range{ "Index too large:" + std::to_string(index) };
// return const_cast<T&>(std::as_const(*this)[index]);
//}
template<typename T>
const T& Array<T>::operator[](size_t index) const
{
if (index >= m_size)
throw std::out_of_range{ "Index too large:" + std::to_string(index) };
return m_elements[index];
}
//template<typename T> //从前的老方法解决const 与非 const T&函数重复的方法
//T& Array<T>::operator[](size_t index)
//{
// return const_cast<T&>(static_cast<const Array<T>&>(*this)[index]);
//}
template<typename T> //从C++17开始的新方法,叫做"变为const又变回来"
T& Array<T>::operator[](size_t index)
{
return const_cast<T&>(std::as_const(*this)[index]);
}
//如何理解如上代码,并且这是极少数被接受使用const_cast<>的地方
//template<typename T>
//T& Array<T>::operator[](size_t index)
//{
// Array<T>& nonConstRef = *this;
// const Array<T>& constRef = std::as_const(nonConstRef);
// const T& constResult = constRef[index];
// return const_cast<T&>(constResult);
//}
//template<typename T>
//Array<T>& Array<T>::operator=(const Array& rhs)
//{
// if (&rhs != this)
// {
// delete[] m_elements;
//
// this->m_size = rhs.m_size;
// this->m_elements = new T[m_size];
// for (size_t i; i < m_size; i++)
// m_elements[i] = rhs.m_elements[i];
// }
// return *this;
//}
template<typename T>
void swap(T& one, T& other) noexcept
{
T copy(one);
one = other;
other = copy;
}
template<typename T>
Array<T>& Array<T>::operator=(const Array& rhs)
{
Array<T> copy{ rhs };
std::swap(copy);
return *this;
}
template<typename T>
void Array<T>::swap(Array& other) noexcept
{
std::swap(m_elements, other.m_elements);
std::swap(m_size, other.m_size);
}
template<typename T>
void swap(Array<T>& one, Array<T>& other) noexcept
{
one.swap(other);
}
代码重复
现在我们来讨论代码重复的问题
在上诉代码中,我们拥有 const和非 const版本的[]运算符重载,它们代码块中的内容一模一样,使得违法了DRY原则,从而增加了维护代码的成本。
在C++11我们是这么解决的:
template<typename T> //从前的老方法解决const 与非 const T&函数重复的方法
T& Array<T>::operator[](size_t index)
{
return const_cast<T&>(static_cast<const Array<T>&>(*this)[index]);
}
上面的代码十分繁琐,读者可以自己解剖一下这段代码。
好消息是在C++17后我们可以这么干:
template<typename T> //从C++17开始的新方法,叫做"变为const又变回来"
T& Array<T>::operator[](size_t index)
{
return const_cast<T&>(std::as_const(*this)[index]);
}
//如何理解如上代码,并且这是极少数被接受使用const_cast<>的地方
//template<typename T>
//T& Array<T>::operator[](size_t index)
//{
// Array<T>& nonConstRef = *this;
// const Array<T>& constRef = std::as_const(nonConstRef);
// const T& constResult = constRef[index];
// return const_cast<T&>(constResult);
//}
以上处理代码重复问题的思想被称为"变为const又变回来",运用了C++17 < utility >库中引入的std::as_const()函数将*this[index]变为const类型,又因为我们的返回类型是非 const,所以最后我们又使用 const_cast来解除 const限定。如果我们不先"变为const"的话就会发生死循环。
异常安全性与复制后交换
观察以下代码,请读者分析其中可能出错的地方:
template<typename T>
Array<T>& Array<T>::operator=(const Array& rhs)
{
if (&rhs != this)
{
delete[] m_elements;
this->m_size = rhs.m_size;
this->m_elements = new T[m_size];
for (size_t i; i < m_size; i++)
m_elements[i] = rhs.m_elements[i];
}
return *this;
}
第一,可能会出错的地方是——可能会因为某些原因无法在自由存储区分配内存而抛出bad_alloc异常。
第二,m_elements[i] = rhs.m_elements[i];这条语句可能会因为任意类型T而导致赋值失败。
所以我们希望我们的函数要么全部成功,要么失败后被初始化或返回到执行前的状态,这样的思想被称为"复制后交换"。
复制后交换模式:
1.创建对象的一个副本。
2.修改这个副本而不是原对象。原对象仍然保持不变。
3.如果全部修改成功,则用副本替换原对象。
在此例中,该思想的实现如下:
//template<typename T> //这是不完全的方法
// void swap(T& one, T& other) noexcept
// {
// T copy(one);
// one = other;
// other = copy;
// }
template<typename T>
Array<T>& Array<T>::operator=(const Array& rhs)
{
Array<T> copy{ rhs };
swap(copy); //使用的是类成员函数swap
return *this;
}
template<typename T>
void Array<T>::swap(Array& other) noexcept
{
std::swap(m_elements, other.m_elements); //使用<utility>库提供的 std::swap()函数
std::swap(m_size, other.m_size); //使用<utility>库提供的 std::swap()函数
}
template<typename T>
void swap(Array<T>& one, Array<T>& other) noexcept //使用swap()成员函数版本实现传统的非成员函数swap()
{
one.swap(other);
}
这里出现了关键字 noexcept,它的作用是在函数中不接收异常的抛出。
模板的显式实例化
在类模板的实例化检查类时,我们必须一一实现类的各个模板函数以测试整个类,这是一个十分麻烦的事情。解决方法就是在确认所编写的类模板全部正确之前可以先显式实例化类模板让其在编译时实现所有模板成员函数的实例化。
template class Array<double>;
非类型的类模板参数
template<typename T,size_t size>
class ClassName
{
//Definition using T and size
};
如上的代码中出现了非类型的类模板参数 size_t,其实也就是说我们可以显式地声明一个参数类型,但这是有条件的,例如我们只能将基本类型作为非类型的类模板参数(int, double, float, size_t, long, 枚举类型,指针类型和引用类型等,但不能是void)。所以我们之前定义的Array类可以添加一个签名为starIndex,int类型的非类型模板参数来实现从startIndex为索引的开始。
如有必要也可以将类型参数作为非类型参数使用,但是value必须放在T的声明之后:
template<typename T,T value>
class ClassName
{
//Definition using T and value
};
当然这就造成一个小问题,我们主动地限制了T的类型必须是非类型模板参数能使用的类型。
非类型参数的实参
当你想为非类型参数添加实参时,实参必须为常量表达式,也就是说非const的参数不会被接受和编译:
const int value{10};
template<typename T,int size>
class MyClass
{
...
}
int main()
{
MyClass<double,size> a; //能成功编译,但如果value是非const时就会错误。同时,如果value是size_t类型,那么编译器会提供实参的标准转换,将传入size的值转换为int类型
}
注:不能修改模板中的参数,当模板被实例化后,其中的T和size便成为了const值,不能对其进行作为左值进行修改。
对比非类型模板实参与构造函数实参
这里要注意,如上面所声明的MyClass类中,不同的size值实际上创造了不同的类:
MyClass<int,1> a;
MyClass<int,2> b;
b=a; //这里将编译失败,因为a与b是不同的类
一般情况下,我们不会在模板中添加非类型参数,而是在构造函数中添加一个赋了值的参数来实现相同的功能:
template<typename T>
class MyClass
{
public:
MyClass(otherType name,size_t size=0);
...
}
模板参数的默认值
在模板参数中,类型参数也可以有默认值,如下:
template<typename T = int , int startIndex = 0>
class A
{
...
};
在实例化类 A 时,如果我们不在尖括号内填入任何参数,那么A将会被默认为 A< int , 0 >。
类模板实参推断
要做到类模板实参自动推断,取决于传入参数是否与T有绑定,从而使编译器能够通过传入的参数来判断类模板的类型。
类模板特化
有时候同一个模板里的成员、成员函数或运算符重载并不适用于模板推断出的所有类型。此时我们就该通过类模板特化来实现特殊情况。
定义类模板特化
语法:
template<>
class Array<TheTypeYouWanne>
{
//类成员
};
完整的类模板特化是类定义而不是类模板。生成类模板特化实例时,不是使用类模板,而是使用为该类型定义的特化。
类模板完整特化不需要和原模板完全相同,你可以把它当成和原模版同名的一个特例来使用。
注:千万不要去特化函数模板,而应该使用函数重载。但相反的是,类模板的特化是相当安全的。
部分模板特化
语法:
template<int startIndex>
class Array<TheTypeYouWanne,startIndex>
{
//类成员
};
我们可以看到,同名的部分模板特化中我们在template后只添加了需要特化的参数int类型的startIndex,因为我们在类的声明处已经特化了Array的类型为TheTypeYouWanne,之后能够根据参数推断而创建的不同类只由startIndex来决定了。
平常,我们也为指针创建类模板特化,语法如下:
template<typename T,int startIndex>
class Array<T*,startIndex>
{
//类成员
};
第一个参数仍旧是T,但模板名后面的尖括号中的T*表示,这个定义用于将T指定为指针类型的实例。其他两个参数仍旧可变,所以这个特化可应用于之前定义的Array模板类实参为指针的实例。
带有嵌套类的类模板
不多BB,上实例!
template<typename T>
class Stack
{
private:
class Node
{
public:
Node(const T& item)
:m_item{item}
{}
T m_item;
Node* m_next {};
};
Node* m_head{};
public:
Stack() = default;
~Stack();
Stack(const Stack& stack);
Stack& operator=(const Stack& rhs);
void swap(Stack& other) noexcept;
void push(const T& item);
T pop();
bool isEmpty() const;
};
template <typename T>
Stack<T>::Stack(const Stack& stack)
{
if(stack.m_head)
{
m_head = new Node{ *stack.m_head };
Node* oldNode{ stack.m_head };
Node* newNode{ m_head };
while(oldNode = oldNode->m_next)
{
newNode->m_next = new Node{ *oldNode };
newNode = newNode->m_next;
}
}
}
template<typename T>
bool Stack<T>::isEmpty() const
{
return m_head == nullptr;
}
template<typename T>
void Stack<T>::swap(Stack& other) noexcept
{
std::swap(m_head, other.m_head);
}
template <typename T>
Stack<T>& Stack<T>::operator=(const Stack& rhs)
{
auto copy{ rhs };
swap(copy);
return *this;
}
template<typename T>
void Stack<T>::push(const T& item)
{
Node* node{ new Node{item} };
node->m_next = m_head;
m_head = node;
}
template<typename T>
T Stack<T>::pop()
{
if(isEmpty())
{
throw std::logic_error{ "Stack is empty" };
}
auto* next{ m_head->m_next };
T item{ m_head->m_item };
delete m_head;
m_head = next;
return item;
}
template <typename T>
Stack<T>::~Stack()
{
while(m_head)
{
//auto* next = m_head->m_next;
//delete m_head;
//m_head = next;
//以上代码被视为代码重复,在书写过程中,我们发现pop函数与析构函数相同,都是先指定一个临时指针来保存m_next的指向,删除m_head,然后使m_head指向下一个对象
while(!isEmpty())
{
pop();
}
}
}
上述代码不难理解,读者慢慢分析就能理解,因为这里的嵌套是简单嵌套,没有涉及很复杂的内容。