C++ Templates之技巧性基础知识
目录
为什么基类的成员在父类中调用必须指明作用域或者使用this->?
模板的全特化(Full specialization of template)
模板的偏特化(template partial specialization)
关键字typename的使用
#include <iostream>
using namespace std;
#include <vector>
template <typename T>
void PrintElement(T const& obj)
{
typename T::const_iterator cpos;
typename T::const_iterator end = obj.cend();
for (cpos = obj.cbegin(); cpos != end; cpos++)
{
cout << *cpos << " ";
}
cout << endl;
}
int main()
{
vector<int> Obj{ 1,2,3,4,5,6,7,8,9,10 };
PrintElement(Obj);
}
有时候T是一个类,类中嵌套了其他类,比如STL标准模板库中vector类中就存在“const_iterator只读迭代器”,”iterator可读写的迭代器”等常用的嵌套类类型。
在我们构建模板时,我们会发现编译器不会给我们任何提示信息,这是由于模板在实例化之前,编译器不知道参数T是什么类型,你在编译器未知的情况下,使用T::const_iterator,人家编译器可不知道你这是什么东西,用::访问的东西多了,类类型当中的static静态成员变量也可以用类类型T加上::作用域访问符来访问。所以我们要让编译器知道T::const_iterator是T类型中嵌套的子类类型就得用typename来声明一下。
举个例子:(说明typename关键字的作用)
在上述程序中,由于存在typename,那么编译器就认为ptr是T::SubType类型的一个指针,但是如果没有typename呢?此时,编译器会将T::SubType看作是T类型的静态成员变量,那么T::SubType * ptr被认为是两个变量的相乘(编译器认为*为乘号✖),如果编译器这样操作的话,你的程序就会报错,接着就凉凉了。
.template构造
#include <iostream>
#include <bitset>
using namespace std;
template <unsigned N>
void PrintBitset(bitset<N> const& obj)
{
cout << obj.to_string<char, char_traits<char>, allocator<char> >() << endl;
}
int main()
{
bitset<4> Obj;
PrintBitset(Obj);
}
看上述这个例子,我们会感觉的到有些不对劲,为什么要加上个”.template”呢?我们先来分析一下这个例子的特点:
① obj是个含有模板参数的类对象;
② to_string是个含有模板参数的类成员函数;
这样的话,为了防止编译器将:
看作“两个成员在比较大小”:
我们这样做:
#include <iostream>
#include <bitset>
using namespace std;
template <unsigned N>
void PrintBitset(bitset<N> const& obj)
{
cout << obj.template to_string<char, char_traits<char>, allocator<char> >() << endl;
}
int main()
{
bitset<4> Obj;
PrintBitset(Obj);
}
在obj类模板调用含有模板参数的类成员函数之前,其实就是在”成员访问运算符(.)”后面插入template关键字,其实我在VS2017中尝试了许多次,感觉加不加.template关键字没啥区别,我感觉《C++ Templates》书中提及的这部分注意事项应该是针对于某些编译器来说的吧,不同的编译器通常情况下执行情况稍有不同,可能会在这里出现错误,我觉得还是加上template关键字最好,这样可以保证你的程序在任何编译器中编译都不会造成错误。
练习小项目
要求:我们有一个N个人的团队,每个团队中的每个工作者有”姓名,每周工作时长(h)和每周的薪水(百元)”,我们想要求出这个团队中的平均薪资最高的那个人的每小时平均薪水和对应的工作人员姓名。
代码示例:
Team.hpp
#include <iostream>
#include <string>
#include <exception>
using namespace std;
template <unsigned N>
class Team;
class Worker
{
template <unsigned N>
friend class Team;
private:
string name;
int NowSalary;
int WorkTime;
public:
Worker() {};
Worker(const Worker& Obj)
{
this->name = Obj.name;
this->NowSalary = Obj.NowSalary;
this->WorkTime = Obj.WorkTime;
}
Worker(string name, int NowSalary, int WorkTime)
{
this->name = name;
this->NowSalary = NowSalary;
this->WorkTime = WorkTime;
}
Worker& operator = (const Worker& Obj)
{
this->name = Obj.name;
this->NowSalary = Obj.NowSalary;
this->WorkTime = Obj.WorkTime;
return *this;
}
Worker& operator = (const Worker&& Obj)
{
this->name = Obj.name;
this->NowSalary = Obj.NowSalary;
this->WorkTime = Obj.WorkTime;
return *this;
}
bool operator > (const Worker& obj) const
{
return this->NowSalary / this->WorkTime > obj.NowSalary / obj.WorkTime;
}
bool operator < (const Worker& obj) const
{
return this->NowSalary / this->WorkTime < obj.NowSalary / obj.WorkTime;
}
};
template <unsigned N>
class Team
{
private:
Worker* workers;
int Pos;
int MaxSalaryPerHour;
string name;
public:
Team()
{
workers = new Worker[N];
Pos = 0;
MaxSalaryPerHour = 0;
name = "无";
}
void Push(const Worker& Obj)
{
if (Pos >= N)
{
throw out_of_range("Over Max Range!");
}
workers[Pos] = Obj;
Pos++;
}
void Pop()
{
if (Pos == 0)
{
throw out_of_range("Empty!");
}
Worker* NewWorkers = new Worker[--Pos];
for (int i = 0; i < Pos; i++)
{
NewWorkers[i] = workers[i];
}
delete[] workers;
workers = NewWorkers;
}
template <typename T, T(*Func)(const T& var1, const T& var2)>
void SearchMax()
{
Worker TheWorker;
for (int i = 0; i < Pos - 1; i++)
{
TheWorker = workers[i] > workers[i + 1] ? workers[i] : workers[i + 1];
}
this->MaxSalaryPerHour = TheWorker.NowSalary / TheWorker.WorkTime;
this->name = TheWorker.name;
cout << "Max Salary Per Hour:" << this->MaxSalaryPerHour << ",The Worker's Name:" << this->name << endl;
}
};
Main.cpp
#include <iostream>
using namespace std;
#include "Worker.hpp"
#include <exception>
Worker Max(const Worker& var1, const Worker& var2)
{
return var1 > var2 ? var1 : var2;
}
int main()
{
Team<4> Obj1;
try
{
Obj1.Push(Worker("张三", 19, 10));
Obj1.Push(Worker("李四", 17, 12));
Obj1.Push(Worker("王五", 10, 2));
Obj1.Push(Worker("赵六", 19, 14));
Obj1.template SearchMax<Worker, Max>();
}
catch (const out_of_range& exp)
{
cout << exp.what() << endl;
}
}
为什么基类的成员在父类中调用必须指明作用域或者使用this->?
代码示例:
#include <iostream>
using namespace std;
template <class T>
class Base
{
public:
void exit()
{
cout << typeid(T).name() << endl;
};
};
template <class T>
class Father: Base<T>
{
public:
void Foo()
{
exit();
}
};
int main()
{
Father<int> Obj1;
Obj1.Foo();
}
运行结果:
我们看到,这个程序中Father类对象Obj1的成员函数Foo并没有调用从Base类中继承的exit函数,我们应该这样改正这个程序(明确exit函数的归属位置,即我们到底要调用那里的exit函数)。
代码示例:
#include <iostream>
using namespace std;
template <class T>
class Base
{
public:
void exit()
{
cout << typeid(T).name() << endl;
};
};
template <class T>
class Father: Base<T>
{
public:
void Foo()
{
this->exit(); // Base<T>::exit()
}
};
int main()
{
Father<int> Obj1;
Obj1.Foo();
}
相较于先前那个程序,我们在exit函数中添加了this->指针/Base<T>::作用域运算符,有了其中一个(this->指针 或 Base<T>::作用域运算符),编译器就可以确定exit函数不是从外边传入的而是Father类自身自带的(this->指针)/从基类Base那里继承的(Base<T>::作用域运算符)。
运行结果:
在《C++ Templates》一书中,有这样一段话讲述了我上面陈述的意思:
成员模板的优势
我们有时候被“不同类型的自定义Stack之间”不可以相互赋值而烦扰,为啥Stack<int>类型的对象不可以调用Stack模板类中的重载=赋值运算符去赋值给Stack<float>类型的对象?
其实,我们重载的=赋值运算符由于是在模板类实例化(Stack<int>)时,已经确定可以使用赋值操作的两个对象的类类型:
Main.cpp
#include "Stack.hpp"
#include <iostream>
using namespace std;
int main()
{
Stack<int> Stack_Obj1(90);
Stack<float> Stack_Obj2;
Stack_Obj2 = Stack_Obj1;
}
Stack.hpp
#include <iostream>
using namespace std;
#include <deque>
template <class T,class CONT = deque<T> >
class Stack
{
private:
CONT element;
public:
Stack() = default;
Stack(const T& obj);
Stack(const Stack& obj);
Stack& operator = (const Stack& obj);
T& operator [] (const int& order);
};
template <class T, class CONT /*= deque<T> */>
T& Stack<T, CONT>::operator[](const int& order)
{
return this->element.at(order);
}
template <class T, class CONT /*= deque<T> */>
Stack<T, CONT>& Stack<T, CONT>::operator=(const Stack& obj)
{
this->element.clear();
this->element.assign(obj.element.begin(), obj.element.end());
}
template <class T, class CONT /*= deque<T> */>
Stack<T, CONT>::Stack(const Stack& obj)
{
this->element.clear();
this->element.assign(obj.begin(), obj.end());
}
template <class T, class CONT /*= deque<T> */>
Stack<T, CONT>::Stack(const T& obj)
{
this->element.clear();
this->element.push_front(obj);
}
运行结果:
为什么会这样,请让我细细道来:
① 首先,我们使用Stack<variable type>获得了一个实例化的模板,每个模板的赋值运算符的两端操作变量都必须是variable type类型的;
② 我们使用“Stack_Obj1 = Stack_Obj2”说明,此时的=赋值运算符的两端操作变量必须是与Stack_Obj1变量类型相同,即Stack<int>类型的;
③ 我们的右端操作变量Stack_Obj2的操作类型是Stack<float>类型的,编译器一下子看懵圈了,我没有一个=赋值运算符可以同时操作两个不同类型的变量相互赋值呀?那我就报错吧!
如何改进呢?这时候,成员模板的优势就出来了:成员模板的主要优势就是“可以自由定义重载运算符的操作对象,从而实现一个操作符同时可以操作两个不同类型的变量”。
改进的代码(Stack.hpp):
#include <iostream>
using namespace std;
#include <deque>
template <class T,class CONT = deque<T> >
class Stack
{
private:
CONT element;
public:
Stack() = default;
Stack(const T& obj);
Stack(const Stack& obj);
template <typename T2, typename CONT2>
Stack& operator = (const Stack<T2, CONT2>& obj);
T& operator [] (const int& order);
T& Top();
void Push(const T& obj);
void Pop();
bool empty();
};
template <class T, class CONT /*= deque<T> */>
bool Stack<T, CONT>::empty()
{
return this->element.empty();
}
template <class T, class CONT /*= deque<T> */>
void Stack<T, CONT>::Pop()
{
if (this->element.empty())
{
throw out_of_range("Empty!");
}
this->element.pop_front();
}
template <class T, class CONT /*= deque<T> */>
void Stack<T, CONT>::Push(const T& obj)
{
this->element.push_front(obj);
}
template <class T, class CONT /*= deque<T> */>
T& Stack<T, CONT>::Top()
{
if (this->element.empty())
{
throw out_of_range("Empty!");
}
return *(this->element.end() - 1);
}
template <typename T, typename CONT>
template <typename T2, typename CONT2>
Stack<T, CONT>& Stack<T, CONT>::operator=(const Stack<T2, CONT2>& obj)
{
if ((void*)this != (void*)&obj)
{
return *this;
}
Stack<T2, CONT2> TempStack(obj);
this->element.clear();
while (!TempStack.empty())
{
this->element.push_front(TempStack.Top());
TempStack.Pop();
}
}
template <class T, class CONT /*= deque<T> */>
T& Stack<T, CONT>::operator[](const int& order)
{
return this->element.at(order);
}
template <class T, class CONT /*= deque<T> */>
Stack<T, CONT>::Stack(const Stack& obj)
{
Stack<T, CONT> TempStack(obj);
this->element.clear();
while (!TempStack.empty())
{
this->element.push_front(TempStack.Top());
TempStack.Pop();
}
}
template <class T, class CONT /*= deque<T> */>
Stack<T, CONT>::Stack(const T& obj)
{
this->element.clear();
this->element.push_front(obj);
}
你可能看得很乱,没关系,这里有一份Stack类的成员函数列表:
class Stack
{
private:
CONT element;
public:
Stack() = default;
Stack(const T& obj);
Stack(const Stack& obj);
template <typename T2, typename CONT2>
Stack& operator = (const Stack<T2, CONT2>& obj);
T& operator [] (const int& order);
T& Top();
void Push(const T& obj);
void Pop();
bool empty();
};
我们知道,当我们传入成员函数的参数是个类对象时,我们无法通过类对象直接访问对象的私有成员数据,我们只能通过类类型提供给我们的外围接口去按照一定的方式访问对象的数据,为了不改变传入类对象的原有属性并且将传入的类对象完完整整的拷贝至我们要操作的类对象中,我们进行如下操作:
① 清空我们要操作的类对象中的数据;
② 创建一个和传入对象类型一致的临时变量,并调用拷贝构造函数在其定义时就将参数的数据拷至临时对象之中;
③ 看到我给大家提供的成员函数列表中,有Top和Pop两个函数最为显眼,我们可以用while循环,先访问临时对象的栈顶元素,并将该元素push_front移入容器当中,然后Pop释放掉栈顶元素,循环往复直至将临时变量中的元素移空为止;
④ 说到“如何判断是否移空临时对象中的元素?”,Stack类中empty成员函数可以帮到我们。
说实在的,我一开始初识Stack自定义类时,我就想为什么不直接操作Stack的成员数据呢?把Stack自定义类类型的成员数据的访问权限设置成public公有权限不香吗?其实这样反倒倒行逆施,我们想要这个程序仅仅按照我们想要的方式去被操作,我们可以将STL容器进行加工处理,取其精华构造成我们想要的“新容器”,但是我们最注重的是“安全,安全,安全!”,只有封装之后,即给操作者暴露出我们想提供的外围接口,才可以保证程序的绝对安全!
模板的特化
其实,模板的特化就是我们常说的重载,只不过我们这里的重载仅仅是模板的类型参数列表不一样,而且最重要的一点就是“模板的特化是针对于类类型来说的”,函数模板没有这一特性:
应用于函数模板时,编译器报错!
模板的全特化(Full specialization of template)
一个模板被称为全特化的条件:1.必须有一个主模板类 2.模板类型被全部明确化。
代码示例:
#include <iostream>
using namespace std;
template <class T>
class Base
{
public:
void ShowInf()
{
cout << "实例化T的类型为" << typeid(T).name() << endl;
}
};
template <>
class Base <int>
{
public:
void ShowInf()
{
cout << "全特化模板,实例化T的类型为int" << endl;
}
};
int main()
{
Base<int> Obj;
Obj.ShowInf();
}
运行结果:
模板的偏特化(template partial specialization)
偏特化就是介于二者之间的模板,它的模板名与主版本模板名相同,但是它的模板型中,有被明确化的部分和没有被明确化的部分。
注意:偏特化的条件:1.必须有一个主模板,2.模板类型被部分明确化。
类型参数为指针时的模板偏特化
代码示例:
#include <iostream>
using namespace std;
template <class T>
class Base
{
public:
void ShowInf()
{
cout << "实例化T的类型为" << typeid(T).name() << endl;
}
};
template <class T> // 如果模板实例化参数T为指针类型,则调用该模板进行实例化操作
class Base <T*>
{
public:
void ShowInf()
{
cout << "偏特化模板,其模板参数为" << typeid(T).name() << endl;
}
};
int main()
{
Base<int*> Obj;
Obj.ShowInf();
}
运行结果:
类型参数部分实例化时的模板偏特化
代码示例:
#include <iostream>
using namespace std;
template <class T1, class T2>
class Base
{
public:
void ShowInf()
{
cout << "普通模板的实例化" << endl;
}
};
template <class T2>
class Base <int, T2>
{
public:
void ShowInf()
{
cout << "偏特化模板" << endl;
}
};
int main()
{
Base<int, int> Obj;
Obj.ShowInf();
}
运行结果:
两个类型参数T1,T2相同时的模板偏特化
代码示例:
#include <iostream>
using namespace std;
template <class T1, class T2>
class Base
{
public:
void ShowInf()
{
cout << "普通模板的实例化" << endl;
}
};
template <class T>
class Base <T, T>
{
public:
void ShowInf()
{
cout << "偏特化模板" << endl;
}
};
int main()
{
Base<int, int> Obj;
Obj.ShowInf();
}
运行结果:
当模板实例化时,模板的全特化,模板的偏特化都适用时的调用顺序:
注意:类类型一旦声明为模板,就不可以存在同名的普通类的版本,即类模板与同名的普通类不可以共存!
优先级排序:全特化类>偏特化类>主版本模板类
代码示例:
#include <iostream>
using namespace std;
template <class T1, class T2>
class Base
{
public:
void ShowInf()
{
cout << "普通模板的实例化" << endl;
}
};
template<>
class Base<int, int>
{
public:
void ShowInf()
{
cout << "全特化模板" << endl;
}
};
template <class T>
class Base <T, T>
{
public:
void ShowInf()
{
cout << "偏特化模板" << endl;
}
};
int main()
{
Base<int, int> Obj;
Obj.ShowInf();
}
运行结果:
函数模板的重载(类似于类模板的特化)
代码示例:
#include <iostream>
using namespace std;
template <typename T>
void ShowInf(T obj)
{
cout << "调用普通模板" << endl;
}
template <>
void ShowInf(int obj)
{
cout << "调用特定类型的函数模板" << endl;
}
void ShowInf(int obj)
{
cout << "调用普通版本的函数版本" << endl;
}
int main()
{
int obj = 10;
ShowInf(obj);
}
运行结果:
从这个例子中,我们可以看出当函数普通版本与函数模板的特化版本均满足要求时,调用顺序如下:
优先级排序:普通函数版本>函数模板特化版本>函数模板主版本
模板的“模板参数”
为什么需要模板的“模板参数”?
最重要的原因是“方便”!如何方便呢?我们用Stack举例说明,如果我们想要允许指定存储Stack元素的容器,是这么做的:
template <typename T, typename Cont = std::vector<T>>
class Stack {
private:
Cont elems; // elements
......
};
当我们实例化Stack类对象时,我们通常进行如下操作:
Stack<double,std::deque<double>> dblStack;
但是这样的缺点是需要指定元素类型两次,然而这两个类型是一样的。
使用模板的模板参数(Template Template Parameters),允许我们在声明Stack类模板的时候只指定容器的类型而不去指定容器中元素的类型。例如:
template <typename T, template <typename Elem> class Cont = std::deque>
class Stack {
private:
Cont<T> elems; // elements
public:
void push(T const &); // push element
void pop(); // pop element
T const &top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
...
};
此时,我们实例化Stack模板时,进行的是如下操作:
Stack<int, std::vector> vStack; // integer stack that uses a vector
与第一种方式的区别是:第二个模板参数是一个类模板。
此时,看起来一切都是那么顺利,真的吗?
举例说明:
#include <iostream>
using namespace std;
#include <vector>
template <typename T, template<typename T> class CONT >
class Base
{
};
int main()
{
Base<int, vector> obj;
}
运行结果:
错误原因:
其实,参数不匹配是有原因的,我们传入的参数就不对,为什么呢?我们回想一下:STL标准容器类型其实是一个模板类类型,实例化STL容器的时候通常是vector<int>,但是其实vector容器中有两个参数第二个参数是“容器空间配置器allocator”。为了完整的匹配vector模板类的类型,我们做如下调整:
#include <iostream>
using namespace std;
#include <vector>
template <typename T, template<typename T, typename ALLOC = allocator<T> > class CONT >
class Base
{
};
int main()
{
Base<int, vector> obj;
}
这样的话,程序运行就完全OK了。
向模板中传入字符串的注意事项
有人问为什么错?我们知道适用字符型数组比较大小,应该遵从“字符型数组元素个数相同”的原则。我们这里的”apple”实参演绎出的T为const char[6]类型的(隐含’\0’字符),而”tomato”实参演绎出的T是const char[7]类型的,这两个字符型数组显然元素个数不相同,在C语言风格的字符数组比较大小规则中,这显然是不可以的。
改正为上述例子就可以了,在C++风格的字符数组比较大小中,可以实现不同元素个数的字符数组的大小比较。这得益于“在没有引用时,实参演绎也就是自动类型推导会丢失一些传入实参的属性,比如:没有引用时,你传入的是字符型数组,数组之间比较大小理应用C语言风格中的字符数组比较大小原则来实现,但是当丢失了“字符型数组的特性”,即你传入的参数被编译器认为是“单纯的字符常量指针const char*”时,编译器会使用C++风格的字符串比较大小规则去比较两个字符串”。
为了改善这个弊病,我们可以做如下几种优化:
① 重载max比较大小的模板函数,从而实现对于参数为从const char[]类型的函数模板进行特例化实现:
#include <iostream>
using namespace std;
template <typename T, unsigned N, unsigned M>
T const* max(T const a[N], T const b[M])
{
return a > b ? a : b;
}
int main()
{
const char ch1[] = "apple", ch2[] = "NewYear";
cout << max<char, 6, 7>(ch1, ch2) << endl;
}
输出结果:
② 使用上述解决方案:利用非引用实参演绎代替引用实参演绎:
这样做可能会导致decay类型退化从而丢失一些重要的特性,并且导致一些无用的拷贝(这些无用的拷贝是类型特性丢失的主要原因),
③ 将输入参数的数据类型改为const T*,当我们声明T为char数据类型时,const T*为const char*类型,这样的话ch1,ch2可以被强制转换为指针类型去除“字符型数组的特性”,与实参演绎中的decay退化操作类似:
#include <iostream>
using namespace std;
template <typename T>
T const* max(const T *a, const T *b)
{
return a > b ? a : b;
}
int main()
{
const char ch1[] = "apple", ch2[] = "NewYear";
cout << max<char>(ch1, ch2) << endl;
}