参数模板
if(pthread_create(&threadId,NULL,_thread_t<StatDataInfo>,(void*)pTempStat) == 0)
template <typename T>
void *_thread_t(void* args)
//void *PredictCategoryTrainData::StatisticProcess(void *arg)
{
//StatDataInfo *pStatInfo = (StatDataInfo*)arg;
T *pStatInfo = (T*)args;
========================================================================================================================================================================================================================
类模板
类成员函数作为pthread_create函数参数
from:http://www.cnblogs.com/shijingxiang/articles/5389294.html
近日需要将线程池封装成C++类,类名为Threadpool。在类的成员函数exec_task中调用pthread_create去启动线程执行例程thread_rounter。编译之后报错如下:
spfs_threadpool.cpp: In member function ‘int Threadpool::exec_task(task*)’:
spfs_threadpool.cpp:174: error: argument of type ‘void* (Threadpool::)(void*)’ does not match ‘void* (*)(void*)’
出现类型不匹配的问题。因为pthread_create需要的参数类型为void* (*)(void*),而thread_rounter作为类的成 员函数时其类型是void* (Threadpool::)(void*)的成员函数指针。我们知道类的成员函数在经过编译器处理之后,会变成带有 this指针参数的全局函数,所以类型注定是不会匹配的。但是如果将thread_rounter声明为static类型,那么编译器会将static形 式的函数,转换成不带this指针的全局函数,所以其类型可以与pthread_create需要的参数类型相匹配。但是类的静态成员函数无法访问类的非 静态成员,不过这可以通过传递this指针解决这个问题。
综上,我的这个问题可以这个样子解决。
出问题之前的代码:
void *thread_rounter(void *)//线程执行函数
{
//直接访问类的成员
}
exec_task函数中调用:
pthread_create(&tid,NULL,thread_rounter,NULL);//启动线程执行例程
修复这个问题的代码:
static void *thread_rounter(void *tmp)/线程执行函数
{
Threadpool *p=(Threadpool *)tmp;
//通过p指针间接访问类的非静态成员
}
exec_task函数中调用:
pthread_create(&tid,NULL,thread_rounter,(void *)this);//启动线程执行例程
----------------------------------------------------------------------------------------------------------------------
在网上搜索一下还有其他的解决方案,摘录如下,为了以示尊重标明文章来源,感谢原文作者。
方案二:
将线程启动函数声明为模板函数。
摘录自:http://hi.baidu.com/angelevil2006/item/e1806ec30574ff11515058d1
template <typename TYPE, void (TYPE::*_RunThread)() > void* _thread_t(void* param)//线程启动函数,声明为模板函数 { TYPE* This = (TYPE*)param; This->_RunThread(); return NULL; } class MyClass { public: MyClass(); void _RunThread(); private: pthread_t tid; }; void MyClass::_RunThread() { this->DoSomeThing(); } MyClass::MyClass() { pthread_create(&tid, NULL, _thread_t<MyClass, &MyClass::_RunThread>, this); } //函数模版不单可以替换类型本身,还能替换类型的成员函数。 //注意: 1、名称只能是_RunThread,不能在指定模版参数的时候修改; // 2、_RunThread只能是public的,除非把_thread_t定义到MyClass的内部。
采取这个方案放入我的项目中:
出问题之前的代码:
void *thread_rounter(void *)//线程执行函数
{
//直接访问类的成员
}
exec_task函数中调用:
pthread_create(&tid,NULL,thread_rounter,NULL);//启动线程执行例程
修复这个问题的代码:
添加public成员函数:
void Run()
{
//该函数替换上述thread_rounter的执行代码
}
thread_rounter修改为全局模板函数:
template <typename TYPE, void (TYPE::*Run)() >
void * thread_rounter(void * param)
{
TYPE *p=(TYPE*)param;
p->Run();
return NULL;
}
exec_task函数中调用:
pthread_create(&tid,NULL,thread_rounter<Threadpool,&Threadpool::Run>,(void *)this);
总结:
解决这个问题的关键在于想方设法使启动函数指针满足void*(*)(void *)类型。
将启动函数改写成static成员函数适用于可以修改类的源代码的情况。
而将启动函数写成模板函数还可以适用于没有类的源代码的情况,自己写一个类,公共继承自原类,添加启动函数为模板函数即可。
================================================================================================================================================================================================================================================================================================================================
pthread_create()的一个错误示例
1 //pthread_create()函数的错误示例 2 //新建线程同时传入线程号、线程号总和和消息 3 #include <stdio.h> 4 #include <pthread.h> 5 #include <stdlib.h> 6 #define NUM_THREADS 8 7 8 void *PrintHello(void *threadid) 9 { 10 int *id_ptr, taskid; 11 sleep(1); 12 id_ptr=(int *)threadid; 13 taskid=*id_ptr; 14 printf("Hello from thread %d\n", taskid); 15 pthread_exit(NULL); 16 } 17 //int t; 18 int main(int argc, char const *argv[]) 19 { 20 pthread_t threads[NUM_THREADS]; 21 int rc; 22 int t; 23 24 for (t = 0; t < NUM_THREADS; ++t) 25 { 26 printf("Creating thread %d\n",t); 27 rc=pthread_create(&threads[t],NULL,(void *)PrintHello, (void *)&t); 28 if(rc!=0){ 29 printf("error. return code is %d\n", rc); 30 exit(-1); 31 } 32 // sleep(1); 33 // pthread_join(threads[t],NULL); 34 } 35 pthread_exit(NULL); 36 }
在这里主线程创建8个子线程,8个子线程都运行同一个函数PrintHello,休眠1s后打印传入的参数为t
运行结果如下,主线程打印完8条"Creating thread"后经过1s左右子线程打印8条"Hello from thread"
如果取消掉32行或33行的注释,则出现正确的结果:
那为什么会这样呢?先贴一张图
这是函数调用过程图。没错,在对于整个进程来说,主线程生成子线程在由子线程执行某个函数,对CPU来说就是函数调用。从第一次的运行结果可以看出,其执行顺序应该是先主线程执行到pthread_exit()然后等待所有的子线程运行,那这样,有8次参数传递,而且都是存放在同一个地址,即调用者栈帧的ebp+8这个位置,而这个存放的是主线程即main函数栈帧中t的位置。所以,经过一次又一次的覆盖,主线程执行完后最终传递给子线程的应该是最后一次变动后的t。
========================================================================================================================================================================================================================
C++模板template用法总结
2017年02月15日 12:37:41 清城无雪 阅读数:44331 标签: c语言 函数 算法 数据结构 更多
个人分类: C++
引言
模板(Template)指C++程序设计设计语言中采用类型作为参数的程序设计,支持通用程序设计。C++ 的标准库提供许多有用的函数大多结合了模板的观念,如STL以及IO Stream。
函数模板
在c++入门中,很多人会接触swap(int&, int&)这样的函数类似代码如下:
void swap(int&a , int& b) { int temp = a; a = b; b = temp; }
但是如果是要支持long,string,自定义class的swap函数,代码和上述代码差不多,只是类型不同,这个时候就是我们定义swap的函数模板,就可以复用不同类型的swap函数代码,函数模板的声明形式如下:
template <class identifier> function_declaration; template <typename identifier> function_declaration;
swap函数模板的声明和定义代码如下:
//method.h template<typename T> void swap(T& t1, T& t2); #include "method.cpp"
//method.cpp template<typename T> void swap(T& t1, T& t2) { T tmpT; tmpT = t1; t1 = t2; t2 = tmpT; }
上述是模板的声明和定义了,那模板如何实例化呢,模板的实例化是编译器做的事情,与程序员无关,那么上述模板如何使用呢,代码如下:
//main.cpp #include <stdio.h> #include "method.h" int main() { //模板方法 int num1 = 1, num2 = 2; swap<int>(num1, num2); printf("num1:%d, num2:%d\n", num1, num2); return 0; }
这里使用swap函数,必须包含swap的定义,否则编译会出错,这个和一般的函数使用不一样。所以必须在method.h文件的最后一行加入#include "method.cpp"。
类模板
考虑我们写一个简单的栈的类,这个栈可以支持int类型,long类型,string类型等等,不利用类模板,我们就要写三个以上的stack类,其中代码基本一样,通过类模板,我们可以定义一个简单的栈模板,再根据需要实例化为int栈,long栈,string栈。
//statck.h template <class T> class Stack { public: Stack(); ~Stack(); void push(T t); T pop(); bool isEmpty(); private: T *m_pT; int m_maxSize; int m_size; }; #include "stack.cpp"
//stack.cpp template <class T> Stack<T>::Stack(){ m_maxSize = 100; m_size = 0; m_pT = new T[m_maxSize]; } template <class T> Stack<T>::~Stack() { delete [] m_pT ; } template <class T> void Stack<T>::push(T t) { m_size++; m_pT[m_size - 1] = t; } template <class T> T Stack<T>::pop() { T t = m_pT[m_size - 1]; m_size--; return t; } template <class T> bool Stack<T>::isEmpty() { return m_size == 0; }
上述定义了一个类模板--栈,这个栈很简单,只是为了说明类模板如何使用而已,最多只能支持100个元素入栈,使用示例如下:
//main.cpp #include <stdio.h> #include "stack.h" int main() { Stack<int> intStack; intStack.push(1); intStack.push(2); intStack.push(3); while (!intStack.isEmpty()) { printf("num:%d\n", intStack.pop()); } return 0; }
模板参数
模板可以有类型参数,也可以有常规的类型参数int,也可以有默认模板参数,例如
template<class T, T def_val> class Stack{...}
上述类模板的栈有一个限制,就是最多只能支持100个元素,我们可以使用模板参数配置这个栈的最大元素数,如果不配置,就设置默认最大值为100,代码如下:
//statck.h template <class T,int maxsize = 100> class Stack { public: Stack(); ~Stack(); void push(T t); T pop(); bool isEmpty(); private: T *m_pT; int m_maxSize; int m_size; }; #include "stack.cpp"
//stack.cpp template <class T,int maxsize> Stack<T, maxsize>::Stack(){ m_maxSize = maxsize; m_size = 0; m_pT = new T[m_maxSize]; } template <class T,int maxsize> Stack<T, maxsize>::~Stack() { delete [] m_pT ; } template <class T,int maxsize> void Stack<T, maxsize>::push(T t) { m_size++; m_pT[m_size - 1] = t; } template <class T,int maxsize> T Stack<T, maxsize>::pop() { T t = m_pT[m_size - 1]; m_size--; return t; } template <class T,int maxsize> bool Stack<T, maxsize>::isEmpty() { return m_size == 0; }
使用示例如下:
//main.cpp #include <stdio.h> #include "stack.h" int main() { int maxsize = 1024; Stack<int,1024> intStack; for (int i = 0; i < maxsize; i++) { intStack.push(i); } while (!intStack.isEmpty()) { printf("num:%d\n", intStack.pop()); } return 0; }
模板专门化
当我们要定义模板的不同实现,我们可以使用模板的专门化。例如我们定义的stack类模板,如果是char*类型的栈,我们希望可以复制char的所有数据到stack类中,因为只是保存char指针,char指针指向的内存有可能会失效,stack弹出的堆栈元素char指针,指向的内存可能已经无效了。还有我们定义的swap函数模板,在vector或者list等容器类型时,如果容器保存的对象很大,会占用大量内存,性能下降,因为要产生一个临时的大对象保存a,这些都需要模板的专门化才能解决。
函数模板专门化
假设我们swap函数要处理一个情况,我们有两个很多元素的vector<int>,在使用原来的swap函数,执行tmpT = t1要拷贝t1的全部元素,占用大量内存,造成性能下降,于是我们系统通过vector.swap函数解决这个问题,代码如下:
//method.h template<class T> void swap(T& t1, T& t2); #include "method.cpp"
#include <vector> using namespace std; template<class T> void swap(T& t1, T& t2) { T tmpT; tmpT = t1; t1 = t2; t2 = tmpT; } template<> void swap(std::vector<int>& t1, std::vector<int>& t2) { t1.swap(t2); }
template<>前缀表示这是一个专门化,描述时不用模板参数,使用示例如下:
//main.cpp #include <stdio.h> #include <vector> #include <string> #include "method.h" int main() { using namespace std; //模板方法 string str1 = "1", str2 = "2"; swap(str1, str2); printf("str1:%s, str2:%s\n", str1.c_str(), str2.c_str()); vector<int> v1, v2; v1.push_back(1); v2.push_back(2); swap(v1, v2); for (int i = 0; i < v1.size(); i++) { printf("v1[%d]:%d\n", i, v1[i]); } for (int i = 0; i < v2.size(); i++) { printf("v2[%d]:%d\n", i, v2[i]); } return 0; }
vector<int>的swap代码还是比较局限,如果要用模板专门化解决所有vector的swap,该如何做呢,只需要把下面代码
template<> void swap(std::vector<int>& t1, std::vector<int>& t2) { t1.swap(t2); }
改为
template<class V> void swap(std::vector<V>& t1, std::vector<V>& t2) { t1.swap(t2); }
就可以了,其他代码不变。
类模板专门化
请看下面compare代码:
//compare.h template <class T> class compare { public: bool equal(T t1, T t2) { return t1 == t2; } };
#include <iostream> #include "compare.h" int main() { using namespace std; char str1[] = "Hello"; char str2[] = "Hello"; compare<int> c1; compare<char *> c2; cout << c1.equal(1, 1) << endl; //比较两个int类型的参数 cout << c2.equal(str1, str2) << endl; //比较两个char *类型的参数 return 0; }
在比较两个整数,compare的equal方法是正确的,但是compare的模板参数是char*时,这个模板就不能工作了,于是修改如下:
//compare.h #include <string.h> template <class T> class compare { public: bool equal(T t1, T t2) { return t1 == t2; } }; template<>class compare<char *> { public: bool equal(char* t1, char* t2) { return strcmp(t1, t2) == 0; } };
main.cpp文件不变,此代码可以正常工作。
模板类型转换
还记得我们自定义的Stack模板吗,在我们的程序中,假设我们定义了Shape和Circle类,代码如下:
//shape.h class Shape { }; class Circle : public Shape { };
然后我们希望可以这么使用:
//main.cpp #include <stdio.h> #include "stack.h" #include "shape.h" int main() { Stack<Circle*> pcircleStack; Stack<Shape*> pshapeStack; pcircleStack.push(new Circle); pshapeStack = pcircleStack; return 0; }
这里是无法编译的,因为Stack<Shape*>不是Stack<Circle*>的父类,然而我们却希望代码可以这么工作,那我们就要定义转换运算符了,Stack代码如下:
//statck.h template <class T> class Stack { public: Stack(); ~Stack(); void push(T t); T pop(); bool isEmpty(); template<class T2> operator Stack<T2>(); private: T *m_pT; int m_maxSize; int m_size; }; #include "stack.cpp"
template <class T> Stack<T>::Stack(){ m_maxSize = 100; m_size = 0; m_pT = new T[m_maxSize]; } template <class T> Stack<T>::~Stack() { delete [] m_pT ; } template <class T> void Stack<T>::push(T t) { m_size++; m_pT[m_size - 1] = t; } template <class T> T Stack<T>::pop() { T t = m_pT[m_size - 1]; m_size--; return t; } template <class T> bool Stack<T>::isEmpty() { return m_size == 0; } template <class T> template <class T2> Stack<T>::operator Stack<T2>() { Stack<T2> StackT2; for (int i = 0; i < m_size; i++) { StackT2.push((T2)m_pT[m_size - 1]); } return StackT2; }
//main.cpp #include <stdio.h> #include "stack.h" #include "shape.h" int main() { Stack<Circle*> pcircleStack; Stack<Shape*> pshapeStack; pcircleStack.push(new Circle); pshapeStack = pcircleStack; return 0; }
这样,Stack<Circle>或者Stack<Circle*>就可以自动转换为Stack<Shape>或者Stack<Shape*>,如果转换的类型是Stack<int>到Stack<Shape>,编译器会报错。
其他
一个类没有模板参数,但是成员函数有模板参数,是可行的,代码如下:
class Util { public: template <class T> bool equal(T t1, T t2) { return t1 == t2; } }; int main() { Util util; int a = 1, b = 2; util.equal<int>(1, 2); return 0; }
甚至可以把Util的equal声明为static,代码如下:
class Util { public: template <class T> static bool equal(T t1, T t2) { return t1 == t2; } }; int main() { int a = 1, b = 2; Util::equal<int>(1, 2); return 0; }
==============================================================================================================================================================================================================================================================================
咳咳。C++11 加入了线程库,从此告别了标准库不支持并发的历史。然而 c++ 对于多线程的支持还是比较低级,稍微高级一点的用法都需要自己去实现,譬如线程池、信号量等。线程池(thread pool)这个东西,在面试上多次被问到,一般的回答都是:“管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复。” 貌似没有问题吧。但是写起程序来的时候就出问题了。
废话不多说,先上实现,然后再啰嗦。(dont talk, show me ur code !)
代码实现
1 #pragma once 2 #ifndef THREAD_POOL_H 3 #define THREAD_POOL_H 4 5 #include <vector> 6 #include <queue> 7 #include <thread> 8 #include <atomic> 9 #include <condition_variable> 10 #include <future> 11 #include <functional> 12 #include <stdexcept> 13 14 namespace std 15 { 16 #define MAX_THREAD_NUM 256 17 18 //线程池,可以提交变参函数或拉姆达表达式的匿名函数执行,可以获取执行返回值 19 //不支持类成员函数, 支持类静态成员函数或全局函数,Opteron()函数等 20 class threadpool 21 { 22 using Task = std::function<void()>; 23 // 线程池 24 std::vector<std::thread> pool; 25 // 任务队列 26 std::queue<Task> tasks; 27 // 同步 28 std::mutex m_lock; 29 // 条件阻塞 30 std::condition_variable cv_task; 31 // 是否关闭提交 32 std::atomic<bool> stoped; 33 //空闲线程数量 34 std::atomic<int> idlThrNum; 35 36 public: 37 inline threadpool(unsigned short size = 4) :stoped{ false } 38 { 39 idlThrNum = size < 1 ? 1 : size; 40 for (size = 0; size < idlThrNum; ++size) 41 { //初始化线程数量 42 pool.emplace_back( 43 [this] 44 { // 工作线程函数 45 while(!this->stoped) 46 { 47 std::function<void()> task; 48 { // 获取一个待执行的 task 49 std::unique_lock<std::mutex> lock{ this->m_lock };// unique_lock 相比 lock_guard 的好处是:可以随时 unlock() 和 lock() 50 this->cv_task.wait(lock, 51 [this] { 52 return this->stoped.load() || !this->tasks.empty(); 53 } 54 ); // wait 直到有 task 55 if (this->stoped && this->tasks.empty()) 56 return; 57 task = std::move(this->tasks.front()); // 取一个 task 58 this->tasks.pop(); 59 } 60 idlThrNum--; 61 task(); 62 idlThrNum++; 63 } 64 } 65 ); 66 } 67 } 68 inline ~threadpool() 69 { 70 stoped.store(true); 71 cv_task.notify_all(); // 唤醒所有线程执行 72 for (std::thread& thread : pool) { 73 //thread.detach(); // 让线程“自生自灭” 74 if(thread.joinable()) 75 thread.join(); // 等待任务结束, 前提:线程一定会执行完 76 } 77 } 78 79 public: 80 // 提交一个任务 81 // 调用.get()获取返回值会等待任务执行完,获取返回值 82 // 有两种方法可以实现调用类成员, 83 // 一种是使用 bind: .commit(std::bind(&Dog::sayHello, &dog)); 84 // 一种是用 mem_fn: .commit(std::mem_fn(&Dog::sayHello), &dog) 85 template<class F, class... Args> 86 auto commit(F&& f, Args&&... args) ->std::future<decltype(f(args...))> 87 { 88 if (stoped.load()) // stop == true ?? 89 throw std::runtime_error("commit on ThreadPool is stopped."); 90 91 using RetType = decltype(f(args...)); // typename std::result_of<F(Args...)>::type, 函数 f 的返回值类型 92 auto task = std::make_shared<std::packaged_task<RetType()> >( 93 std::bind(std::forward<F>(f), std::forward<Args>(args)...) 94 ); // wtf ! 95 std::future<RetType> future = task->get_future(); 96 { // 添加任务到队列 97 std::lock_guard<std::mutex> lock{ m_lock };//对当前块的语句加锁 lock_guard 是 mutex 的 stack 封装类,构造的时候 lock(),析构的时候 unlock() 98 tasks.emplace( 99 [task]() 100 { // push(Task{...}) 101 (*task)(); 102 } 103 ); 104 } 105 cv_task.notify_one(); // 唤醒一个线程执行 106 107 return future; 108 } 109 110 //空闲线程数量 111 int idlCount() { return idlThrNum; } 112 113 }; 114 115 } 116 117 #endif
代码不多吧,上百行代码就完成了 线程池, 并且, 看看 commit, 哈, 不是固定参数的, 无参数数量限制! 这得益于可变参数模板.
怎么使用?
看下面代码(展开查看)
1 #include "threadpool.h" 2 #include <iostream> 3 4 void fun1(int slp) 5 { 6 printf(" hello, fun1 ! %d\n" ,std::this_thread::get_id()); 7 if (slp>0) { 8 printf(" ======= fun1 sleep %d ========= %d\n",slp, std::this_thread::get_id()); 9 std::this_thread::sleep_for(std::chrono::milliseconds(slp)); 10 } 11 } 12 13 struct gfun { 14 int operator()(int n) { 15 printf("%d hello, gfun ! %d\n" ,n, std::this_thread::get_id() ); 16 return 42; 17 } 18 }; 19 20 class A { 21 public: 22 static int Afun(int n = 0) { //函数必须是 static 的才能直接使用线程池 23 std::cout << n << " hello, Afun ! " << std::this_thread::get_id() << std::endl; 24 return n; 25 } 26 27 static std::string Bfun(int n, std::string str, char c) { 28 std::cout << n << " hello, Bfun ! "<< str.c_str() <<" " << (int)c <<" " << std::this_thread::get_id() << std::endl; 29 return str; 30 } 31 }; 32 33 int main() 34 try { 35 std::threadpool executor{ 50 }; 36 A a; 37 std::future<void> ff = executor.commit(fun1,0); 38 std::future<int> fg = executor.commit(gfun{},0); 39 std::future<int> gg = executor.commit(a.Afun, 9999); //IDE提示错误,但可以编译运行 40 std::future<std::string> gh = executor.commit(A::Bfun, 9998,"mult args", 123); 41 std::future<std::string> fh = executor.commit([]()->std::string { std::cout << "hello, fh ! " << std::this_thread::get_id() << std::endl; return "hello,fh ret !"; }); 42 43 std::cout << " ======= sleep ========= " << std::this_thread::get_id() << std::endl; 44 std::this_thread::sleep_for(std::chrono::microseconds(900)); 45 46 for (int i = 0; i < 50; i++) { 47 executor.commit(fun1,i*100 ); 48 } 49 std::cout << " ======= commit all ========= " << std::this_thread::get_id()<< " idlsize="<<executor.idlCount() << std::endl; 50 51 std::cout << " ======= sleep ========= " << std::this_thread::get_id() << std::endl; 52 std::this_thread::sleep_for(std::chrono::seconds(3)); 53 54 ff.get(); //调用.get()获取返回值会等待线程执行完,获取返回值 55 std::cout << fg.get() << " " << fh.get().c_str()<< " " << std::this_thread::get_id() << std::endl; 56 57 std::cout << " ======= sleep ========= " << std::this_thread::get_id() << std::endl; 58 std::this_thread::sleep_for(std::chrono::seconds(3)); 59 60 std::cout << " ======= fun1,55 ========= " << std::this_thread::get_id() << std::endl; 61 executor.commit(fun1,55).get(); //调用.get()获取返回值会等待线程执行完 62 63 std::cout << "end... " << std::this_thread::get_id() << std::endl; 64 65 66 std::threadpool pool(4); 67 std::vector< std::future<int> > results; 68 69 for (int i = 0; i < 8; ++i) { 70 results.emplace_back( 71 pool.commit([i] { 72 std::cout << "hello " << i << std::endl; 73 std::this_thread::sleep_for(std::chrono::seconds(1)); 74 std::cout << "world " << i << std::endl; 75 return i*i; 76 }) 77 ); 78 } 79 std::cout << " ======= commit all2 ========= " << std::this_thread::get_id() << std::endl; 80 81 for (auto && result : results) 82 std::cout << result.get() << ' '; 83 std::cout << std::endl; 84 return 0; 85 } 86 catch (std::exception& e) { 87 std::cout << "some unhappy happened... " << std::this_thread::get_id() << e.what() << std::endl; 88 }
为了避嫌,先进行一下版权说明:代码是 me “写”的,但是思路来自 Internet, 特别是这个线程池实现(基本 copy 了这个实现,加上这位同学的实现和解释,好东西值得 copy ! 然后综合更改了下,更加简洁)。
实现原理
接着前面的废话说。“管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复。” 这个思路有神马问题?线程池一般要复用线程,所以如果是取一个 task 分配给某一个 thread,执行完之后再重新分配,在语言层面基本都是不支持的:一般语言的 thread 都是执行一个固定的 task 函数,执行完毕线程也就结束了(至少 c++ 是这样)。so 要如何实现 task 和 thread 的分配呢?
让每一个 thread 都去执行调度函数:循环获取一个 task,然后执行之。
idea 是不是很赞!保证了 thread 函数的唯一性,而且复用线程执行 task 。
即使理解了 idea,代码还是需要详细解释一下的。
- 一个线程 pool,一个任务队列 queue ,应该没有意见;
- 任务队列是典型的生产者-消费者模型,本模型至少需要两个工具:一个 mutex + 一个条件变量,或是一个 mutex + 一个信号量。mutex 实际上就是锁,保证任务的添加和移除(获取)的互斥性,一个条件变量是保证获取 task 的同步性:一个 empty 的队列,线程应该等待(阻塞);
- atomic<bool> 本身是原子类型,从名字上就懂:它们的操作 load()/store() 是原子操作,所以不需要再加 mutex。
c++语言细节
即使懂原理也不代表能写出程序,上面用了众多c++11的“奇技淫巧”,下面简单描述之。
- using Task = function<void()> 是类型别名,简化了 typedef 的用法。function<void()> 可以认为是一个函数类型,接受任意原型是 void() 的函数,或是函数对象,或是匿名函数。void() 意思是不带参数,没有返回值。
- pool.emplace_back([this]{...}) 和 pool.push_back([this]{...}) 功能一样,只不过前者性能会更好;
- pool.emplace_back([this]{...}) 是构造了一个线程对象,执行函数是拉姆达匿名函数 ;
- 所有对象的初始化方式均采用了 {},而不再使用 () 方式,因为风格不够一致且容易出错;
- 匿名函数: [this]{...} 不多说。[] 是捕捉器,this 是引用域外的变量 this指针, 内部使用死循环, 由cv_task.wait(lock,[this]{...}) 来阻塞线程;
- delctype(expr) 用来推断 expr 的类型,和 auto 是类似的,相当于类型占位符,占据一个类型的位置;auto f(A a, B b) -> decltype(a+b) 是一种用法,不能写作 decltype(a+b) f(A a, B b),为啥?! c++ 就是这么规定的!
- commit 方法是不是略奇葩!可以带任意多的参数,第一个参数是 f,后面依次是函数 f 的参数!(注意:参数要传struct/class的话,建议用pointer,小心变量的作用域) 可变参数模板是 c++11 的一大亮点,够亮!至于为什么是 Arg... 和 arg... ,因为规定就是这么用的!
- commit 直接使用只能调用stdcall函数,但有两种方法可以实现调用类成员,一种是使用 bind: .commit(std::bind(&Dog::sayHello, &dog)); 一种是用 mem_fn: .commit(std::mem_fn(&Dog::sayHello), &dog);
- make_shared 用来构造 shared_ptr 智能指针。用法大体是 shared_ptr<int> p = make_shared<int>(4) 然后 *p == 4 。智能指针的好处就是, 自动 delete !
- bind 函数,接受函数 f 和部分参数,返回currying后的匿名函数,譬如 bind(add, 4) 可以实现类似 add4 的函数!
- forward() 函数,类似于 move() 函数,后者是将参数右值化,前者是... 肿么说呢?大概意思就是:不改变最初传入的类型的引用类型(左值还是左值,右值还是右值);
- packaged_task 就是任务函数的封装类,通过 get_future 获取 future , 然后通过 future 可以获取函数的返回值(future.get());packaged_task 本身可以像函数一样调用 () ;
- queue 是队列类, front() 获取头部元素, pop() 移除头部元素;back() 获取尾部元素,push() 尾部添加元素;
- lock_guard 是 mutex 的 stack 封装类,构造的时候 lock(),析构的时候 unlock(),是 c++ RAII 的 idea;
- condition_variable cv; 条件变量, 需要配合 unique_lock 使用;unique_lock 相比 lock_guard 的好处是:可以随时 unlock() 和 lock()。 cv.wait() 之前需要持有 mutex,wait 本身会 unlock() mutex,如果条件满足则会重新持有 mutex。
- 最后线程池析构的时候,join() 可以等待任务都执行完在结束,很安全!
Git
代码保存在git,这里可以获取最新代码: https://github.com/lzpong/threadpool
====================================================================================================================================================================================================================================================================================================================================================================
《C++ Template》对Template各个方面进行了较为深度详细的解析,故而本系列博客按书本的各章顺序编排,并只作为简单的读书笔记,详细讲解请购买原版书籍(绝对物超所值)。
------------------------------------------------------------------------------------------------------------
第一章 前言
1.4 编程风格
(1)对“常整数”趋向使用“int const”,而不是使用“const int”。“恒定不变部分”指的是const限定符前面的部分。
------------------------------------------------------------------------------------------------------------
第1部分 基础
第2章 函数模板
2.1 初探函数模板
2.1.1 定义模板
template <typename T> inline T const& max(T const& a, T const& b) { return a < b ? : b a; }
注:你可以使用任何类型(基本类型、类等)来实例化该类型参数,只要所用类型提供模板使用的操作就可以。
2.1.2 使用模板
max(32, 43);
注:通常而言,并不是把模板编译成一个可以处理任何类型的单一实体;而是对于实例化模板参数的每种类型,都从模板产生一个不同的实体。这种用具体类型代替模板参数的过程叫做实例化,它产生了一个模板的实例。
于是,我们可以得出一个结论:模板被编译了两次,分别发生在:
(1)实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。
(2)在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用(该类型没有提供模板所需要使用到的操作)等。
2.2 实参的演绎(deduction)
注:模板实参不允许进行自动类型转换;每个T都必须正确地匹配。如:
max(4, 4.3); // Error:第1个参数类型是int,第2个参数类型是double
2.3 模板参数
函数模板有两种类型的参数(牢记):
(1)模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明:
template <typename T> // T是模板参数
(2)调用参数:位于函数模板名称之后,在一对圆括号内部进行声明:
...max(T const& a, T const& b); // a和b都是调用参数
注:由于函数模板历史发展过程中的一个失误,导致目前(2016/1/11)我们不能在函数模板内部指定缺省的模板实参(形如“template <typename T = xxx>”,不能指定xxx;)。但依然可以指定函数模板的调用实参(形如“...max(T const& a, T const& b = yyy)”,可以指定yyy)(以后应该会支持函数模板指定缺省模板实参)。
注:切记“模板参数”和“模板实参”的区别;函数的“模板参数”和“调用参数”、“模板实参”和“调用实参”的区别;“类型参数”和“非类型参数”的区别;
函数模板和类模板区别:函数模板可以进行模板实参演绎(不能演绎返回类型)、重载、指定缺省调用实参、全局特化;不能指定缺省模板实参,不能局部特化;类模板可以指定缺省模板实参、指定全局特化和局部特化(用来完成类似函数模板重载功能);不能重载类模板,不能进行实参演绎。
(3)显式实例化:当模板参数和调用参数没有发生关联,或者不能由调用参数来决定模板参数的时候,在调用时就必须显式指定模板实参。切记,模板实参演绎并不适合返回类型。如下:
template <typename T1, typename T2, typename RT> inline RT max(T1 const& a, T2 const& b);
那么必须进行显式实例化:
max<int, double, double>(4, 4.3); // OK,但很麻烦。这里T1和T2是不同的类型,所以可以指定两个不同类型的实参4和4.3
注:通常而言,你必须指定“最后一个不能被隐式演绎的模板实参之前的”所有实参类型。上面的例子中,改变模板参数的声明顺序,那么调用者就只需要指定返回类型:
template <typename RT, typename T1, typename T2> inline RT max(T1 const& a, T2 const& b); ... max<double>(4, 4.3); // ok,返回类型是double
2.4 重载函数模板
注:对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会优先调用非模板函数,而不会从该模板产生出一个实例。然而,如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
注:可以显式地指定一个空的模板实参列表,这个语法好像是告诉编译器:只有模板才能匹配这个调用(即便非模板函数更符合匹配条件也不会被调用到),而且所有的模板参数都应该根据调用实参演绎出来。
注:因为模板是不允许自动类型转化的;但普通函数可以进行自动类型转换,所以当一个匹配既没有非模板函数,也没有函数模板可以匹配到的时候,会尝试通过自动类型转换调用到非模板函数(前提是可以转换为非模板函数的参数类型)。
注:在所有重载的实现里面,我们都是通过引用来传递每个实参的。一般而言,在重载函数模板的时候,最好只是改变那些需要改变的内容;就是说,你应该把你的改变限制在下面两种情况:改变参数的数目或者显式地指定模板参数。否则可能会出现非预期的结果。(参考书本2.4节例子)
注:定义一个重载函数A,而在A1(函数A的重载)中调用A,但是,如果直到A1的定义处还没有见到A的定义(也即函数A的定义在函数A1的后面,但函数A1中调用了函数A),那么并不会调用到这个重载函数A,而会寻找在函数A1之前已经定义了的符合条件的其他函数Ax(即便A是符合条件的非模板函数,而Ax是模板函数,也会由于A的声明太迟,而选择调用Ax)。
------------------------------------------------------------------------------------------------------------
第3章 类模板
3.1 类模板Stack的实现
见书源码;
3.1.1 类模板的声明
template <typename T> //可以使用class代替typename class Stack { ... };
注:这个类的类型是Stack<T>,其中T是模板参数。因此,当在声明中需要使用该类的类型时,你必须使用Stack<T>。然而,当使用类名而不是类的类型时,就应该只用Stack;譬如,当你指定类的名称、类的构造函数、析构函数时,就应该使用Stack。
3.1.2 成员函数的实现
为了定义类模板的成员函数,你必须指定该成员函数是一个函数模板,而且你还需要使用这个类模板的完整类型限定符。如下:
template <typename T> void Stack<T>::push(T const& elem) { elems.push_back(elem); }
显然,对于类模板的任何成员函数,你都可以把它实现为内联函数,将它定义于类声明里面,如:
template <typename T> class Stack { ... void push(T const& elem) { elems.push_back(elem); } ... };
3.2 类模板Stack的使用
为了使用类模板对象,必须显式地指定模板实参。
注:
1. 只有那些被调用的成员函数,才会产生这些函数的实例化代码。对于类模板,成员函数只有在被使用的时候才会被实例化。显然,这样可以节省空间和时间;
2. 另一个好处是,对于那些“未能提供所有成员函数中所有操作的”类型,你也可以使用该类型来实例化类模板,只要对那些“未能提供某些操作的”成员函数,模板内部不使用就可以。
3. 如果类模板中含有静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。
切记,要作为模板参数类型,唯一的要求就是:该类型必须提供被调用的所有操作。
3.3 类模板的特化
为了特化一个类模板,你必须在起始处声明一个template<>,接下来声明用来特化类模板的类型。这个类型被用作模板实参,且必须在类名的后面直接指定:
template<> class Stack<std::string> { ... };
进行类模板的特化时,每个成员函数都必须重新定义为普通函数,原来模板函数中的每个T也相应地被进行特化的类型取代。如:
void Stack<std::string>::push(std::string const& elem) { elems.push_back(elem); }
3.4 局部特化
例子如下:
// 两个模板参数具有相同的类型 template <typename T> class Myclass<T, T> // { }; // 第2个模板参数的类型是int template <typename T> class Myclass<T, int> { }; // 两个模板参数都是指针类型 template <typename T1, typename T2> class Myclass<T1*, T2*> // 也可以使引用类型T&,常引用等 { };
3.5 缺省模板实参
对于类模板,你还可以为模板参数定义缺省值;这些值就被称为缺省模板实参;而且它们还可以引用之前的模板参数。(STL容器使用缺省默认实参指定内存分配其alloc)如:
template <typename T, typename CONT = std::vector<T> > class Stack { };
------------------------------------------------------------------------------------------------------------
第4章 非类型模板参数
4.1 非类型的类模板参数
如下定义:
template <typename T, int MAXSIZE> class Stack { }; // 使用 Stack<int, 20> int20Stack; // 可以存储20个int元素的栈 Stack<int, 40> int40Stack; // 可以存储40个int元素的栈
注:每个模板实例都具有自己的类型,因此int20Stack和int40Stack属于不同的类型,而且这两种类型之间也不存在显式或者隐式的类型转换;所以它们之间不能互相替换,更不能互相赋值。
然而,如果从优化设计的观点来看,这个例子并不适合使用缺省值。缺省值应该是直观上正确的值。但对于栈的类型和大小而言,int类型和最大容量100从直观上看起来都不是正确的。因此,在这里最好还是让程序员显式地指定这两个值。因此我们可以在设计文档中用一条声明来说明这两个属性(即类型和最大容量)。
4.2 非类型的函数模板参数
如下定义:
template <typename T, int VAL> T addValue(T const& x) { return x + VAL; }
借助标准模板库(STL)使用上面例子:
std::transform(source.begin(), source.end(), dest.begin(), addValue<int, 5>);
注:
1. 上面的调用中,最后一个实参实例化了函数模板addValue(),它让int元素增加5.
2. 这个例子有一个问题:addValue<int, 5>是一个函数模板实例,而函数模板实例通常被看成是用来命名一组重载函数的集合(即使该组只有一个函数)。然而,根据现今的标准,重载函数的集合并不能被用于模板参数的演绎(注意,标准模板库中的函数是使用模板定义的,故而在transform()函数中,参数是作为函数模板调用实参传递的,也即参与了模板参数演绎)。于是,你必须将这个函数模板的实参强制类型转换为具体的类型:
std::transform(source.begin(), source.end(), dest.begin(), (int(*)(int const&))addValue<int, 5>);
4.3 非类型模板参数的限制
我们还应该知道:非类型模板参数是有限制的。通常而言,它们可以是常整数(包括枚举值)或者指向外部链接对象的指针。
注:浮点数和类对象(class-type)是不允许作为非类型模板参数的。之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板实参是有历史原因的。然而以后可能会支持这个特性。另外,由于字符串文字是内部链接对象(因为两个具有相同名称但出于不同模块的字符串,是两个完全不同的对象),所以你不能使用它们来作为模板实参,如下:
template <char const* name> class MyClass { }; MyClass<"hello"> x; // ERROR:不允许使用字符文字"hello" //另外,你也不能使用全局指针作为模板参数: template <char const* name> class MyClass { ... }; char const* s = "hello"; MyClass<s> x; // s是一个指向内部链接对象的指针 //然而,你可以这样使用: template <char const* name> class MyClass { ... }; extern char const s[] = "hello"; MyClass<s> x; // OK //全局字符数组s由“hello”初始化,是一个外部链接对象。
------------------------------------------------------------------------------------------------------------
第5章 技巧性基础知识
5.1 关键字typename
在C++标准化过程中,引入关键字typename是为了说明:模板内部的标识符可以是一个类型。如下:
template <typename T> class MyClass { // 这里的typename被用来说明:T::SubType是定义于类T内部的一种类型 typename T::SubType* ptr; ... };
注:本节同时提到了一个奇特的构造“.template”:只有当该前面存在依赖于模板参数的对象时,我们才需要在模板内部使用“.template”标记(和类似的诸如->template的标记),而且这些标记也只能在模板中才能使用。如下例子:
void printBitset(std::bitset<N> const& bs) { // 如果没有使用这个template,编译器将不知道下列事实:bs.template后面的小于号(<)并不是数学 // 中的小于号,而是模板实参列表的起始符号。只有在编译器判断小于号之前,存在依赖于模板参数的构造 // 才会出现这种问题。在这个例子中,传入参数bs就是依赖于模板参数N的构造 std::cout << bs.template to_string<char, char_traits<char>, allocator<char> >(); }
5.2 使用this->
考虑例子:
template <typename T> class Base { public: void exit(); }; template <typename T> class Derived : Base<T> // 模板基类 { public: void foo() { exit(); // 调用外部的exit()或者出现错误,而不会调用模板基类的exit() } };
注意:对于那些在基类中声明,并且依赖于模板参数的符号(函数或者变量等),你应该在它们前面使用this->或者Base<T>::限定符。如果希望完全避免不确定性,你可以(使用诸如this->和Base<T>::等)限定(模板中)所有的成员访问。(这两种限定符的详细信息会在本系列文章后面讲解)
参见博文Effective C++ —— 模板与泛型编程(七) 条款43
5.3 成员模板
对于类模板而言,其实例化只有在类型完全相同才能相互赋值。我们通过定义一个身为模板的赋值运算符(成员模板),来达到两个不同类型(但类型可以转换)的实例进行相互赋值的目的,如下声明:
template <typename T> class Stack { ... template <typename T2> Stack<T>& operator= (Stack<T2> const&); };
参见博文Effective C++ —— 模板与泛型编程(七) 条款45
5.4 模板的模板参数
还是以Stack为例:
template <typename T, template <typename ELEM, typename ALLOC = std::allocator<ELEM> > class CONT = std::deque > class Stack { ... };
注:
1. 上面作为模板参数里面的class 不能用typename代替;
2. 还有一个要知道:函数模板并不支持模板的模板参数。
3. 之所以需要定义“ALLOC”,是因为模板的模板实参“std::deque”具有一个缺省模板参数,为了精确匹配模板的模板参数;
5.5 零初始化
对于int、double或者指针等基本类型,并不存在“用一个有用的缺省值来对它们进行初始化”的缺省构造函数;相反,任何未被初始化的局部变量都具有一个不确定值。如果我们希望我们的模板类型的变量都已经用缺省值初始化完毕,那么针对内建类型,我们需要做一些处理,如下:
// 函数模板 template <typename T> void foo() { T x = T(); // 如果T是内建类型,x是0或者false }; // 类模板:初始化列表来初始化模板成员 template <typename T> class MyClass { private: T x; public: MyClass() : x() {} // 确认x已被初始化,内建类型对象也是如此 };
5.6 使用字符串作为函数模板的实参
有时,把字符串传递给函数模板的引用参数会导致出人意料的运行结果:
#include <string> // 注意,method1:引用参数 template <typename T> inline T const& max(T const& a, T const& b) { return a < b ? b : a; } // method2:非引用参数 template <typename T> inline T max2(T a, T b) { return a < b ? b : a; } int main() { std::string s; // 引用参数 ::max("apple", "peach"); // OK, 相同类型的实参 ::max("apple", "tomato"); // ERROR, 不同类型的实参 ::max("apple", s); // ERROR, 不同类型的实参 // 非引用参数 ::max2("apple", "peach"); // OK, 相同类型的实参 ::max2("apple", "tomato"); // OK, 退化(decay)为相同类型的实参 ::max2("apple", s); // ERROR, 不同类型的实参 }
上面method1的问题在于:由于长度的区别,这些字符串属于不同的数值类型。也就是说,“apple”和“peach”具有相同的类型char const[6];然而“tomato”的类型则是char const[7]。
method2调用正确的原因是:对于非引用类型的参数,在实参演绎的过程中,会出现数组到指针的类型转换(这种转型通常也被称为decay)。
小结:
如果你遇到一个关于字符数组和字符指针之间不匹配的问题,你会意外地发现和这个问题会有一定的相似之处。这个问题并没有通用的解决方法,根据不同情况,你可以:
1. 使用非引用参数,取代引用参数(然而,这可能会导致无用的拷贝);
2. 进行重载,编写接收引用参数和非引用参数的两个重载函数(然而,这可能会导致二义性);
3. 对具体类型进行重载(譬如对std::string进行重载);
4. 重载数组类型,譬如:
template <typename T, int N, int M> T const* max (T const (&a)[N], T const (&b)[M]) { return a < b ? b : a; }
5. 强制要求应用程序程序员使用显式类型转换。
对于我们的例子,最好的方法是为字符串重载max().无论如何,为字符串提供重载都是必要的,否则比较的将是两个字符串的地址。
------------------------------------------------------------------------------------------------------------
第6章 模板实战
6.1 包含模型
6.1.1 连接器错误
大多数C和C++程序员会这样组织他们的非模板代码:
1. 类(class)和其他类型(other type)都被放在一个头文件中。通常而言,头文件是一个扩展名为.hpp(或者.H, .h, .hh, hxx)的文件;
2. 对于全局变量和(非内联)函数,只有声明放在头文件中,定义则位于dot-C文件。通常而言,dot-C文件是指扩展名为.cpp(或者.C, .c, .cc, .cxx)的文件。
这样一切都可以正常运作了。所需的类型定义在整个程序中都是可见的;并且对于变量和函数而言,链接器也不会给出重复定义的错误。
但这种情况在模板中会出现一些问题,如下:
// ----------------------------------------------------------- //basics/myfirst.hpp #ifndef MYFIRST_HPP #define MYFIRST_HPP // 模板声明 template <typename T> void print_typeof(T const&) #endif // MYFIRST_HPP // ----------------------------------------------------------- //basics/myfirst.cpp #include <iostream> #include <typeinfo> #include "myfirst.hpp" // 模板的实现/定义 template <typename T> void print_typeof(T const& x) { std::cout << typeid(x).name() << std::endl; } // ----------------------------------------------------------- //basics/myfirstmain.cpp #include "myfirst.cpp" // 使用模板 int main() { double ice = 3.0; print_typeof(ice); // 调用参数类型为double的函数模板 }
大多数C++编译器都会顺利地接受这个程序;但是链接器可能会报错,提示找不到函数print_typeof()的定义。
事实上,这个错误的原因在于:函数模板print_typeof()的定义还没有被实例化。为了使模板真正得到实例化,编译器必须知道:应该实例化哪个定义以及要基于哪个模板实参来进行实例化。遗憾的是,在前面的例子里,这两部分信息位于分开编译的不同文件里面。因此,当我们的编译器看到print_typeof()调用,但还没有看到(基于double实例化的)函数定义的时候(在这个时候,或者说在这个条件下),它只能假设在别处提供了这个定义(但它不知道是哪里提供了),并产生一个指向该定义的引用(这个引用是用来指向该定义的,只不过它目前无法确定,或者说还没有给这个引用赋值,只能让链接器利用该引用来解决这个问题)。另一方面,当编译器处理文件myfirst.cpp的时候,它并没有指出:编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化(个人理解:前面编译器把一个引用提供给了链接器,希望链接器能解决“找不到函数定义”的问题。如果对于普通函数,那么当编译器处理文件myfirst.cpp的时候,是可以确定函数定义的,虽然这个函数定义产生于另一个翻译单元,但可被链接器找到;但这里,我们在myfirst.cpp中定义的是一个函数模板定义,并且也没有指出“编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化”(没有显示实例化地指出应该根据哪个实参进行实例化),这样的话,函数模板的定义依然是不确定的,链接器在此时便报了找不到函数定义的错误)。
要解决上面的问题,可以从两个点入手:
(1)解决找不到函数模板定义问题(包含模型);
(2)解决没有指出“编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化”问题(显示实例化)。
6.1.2 头文件中的模板
对于前面的问题,我们通常是采取对待宏或内联函数的解决方法:我们把模板的定义也包含在声明模板的头文件里面,即让定义和声明都位于同一个头文件中。我们称模板的这种组织方式为包含模型。针对包含模型的组织方式,我们可以得出:包含模型明显增加了包含头文件myfirst.hpp的开销。
从包含模型得出的另一个结论是:非内联函数模板与“内联函数和宏”有一个很重要的区别,那就是非内联函数模板在调用的位置并不会被扩展,而是当它们基于某种类型进行实例化之后,才产生一份新的(基于该类型的)函数拷贝(所以对于非内联函数模板而言,实例化之后才能确定为一个针对特定类型的函数)。
最后,我们需要指出的是:在我们的例子中应用到普通函数模板的所有特性,对类模板的成员函数和静态数据成员、成员函数模板也都是适用的。
6.2 显式实例化
包含模型能够确保所有需要的模板都已经实例化。这是因为:当需要进行实例化的时候,C++编译系统会自动产生所对应的实例化体。另外,C++标准还提供了一种手工实例化模板的机制:显式实例化指示符。
6.2.1 显式实例化的例子
为了说明手工实例化,让我们回顾前面那个导致链接器错误的例子。在此,为了避免这个链接期错误,我们可以通过给程序添加下面的文件:
//basics/myfirstinst.cpp #include "myfirst.cpp" // 基于类型double显式实例化print_typeof() template void print_typeof<double>(double const&);
显式实例化指示符由关键字template和紧接其后的我们所需要实例化的实体(可以是类、函数、成员函数等)的声明组成,而且,该声明是一个已经用实参完全(注意,是完全)替换参数之后的声明。该指示符也适用于成员函数和静态数据成员,如:
// 基于int显式实例化MyClass<>的构造函数 template MyClass<int>::MyClass(); // 基于int显式实例化函数模板max() template int const& max(int const&, int const&);
你还可以显式实例化类模板,这样就可以同时实例化它的所有类成员。但有一点需要注意:对于那些在前面已经实例化过的成员,就不能再次对它们进行实例化(针对每个不同实体,不能存在多个显式实例化体,同时显式实例化体和模板特化也只能二者选其一)。
// 基于int显式实例化类Stack<> template class Stack<int> // 实例化它的所有类成员 // 错误,对于int,不能再次对它进行显式实例化 template Stack<int>:::Stack(); // 基于string显式实例化Stack<>的某些成员函数 template Stack<std::string>::Stack(); template void Stack<std::string>::push(std::string const&); template std::string Stack<std::string>::top() const;
人工实例化有一个显著的缺点:我们必须仔细跟踪每个需要实例化的实体。对于大项目而言,这种跟踪会带来巨大负担,因此,我们并不建议使用这种方法。其优点在于,显式实例化可以精确控制模板实例的准确位置。
6.2.2 整合包含模型和显式实例化
将模板的定义和模板的声明放在两个不同的文件中。通常的做法是使用头文件来表示这两个文件。如下:
// stack.hpp #ifndef STACK_HPP #define STACK_HPP #include <vector> template <typename T> class Stack { private: std::vector<T> elems; public: Stack(); void push(T const&); void pop(); T top() const; }; #endif // stackdef.hpp #ifndef STACKDEF_HPP #define STACKDEF_HPP #include "stack.hpp" template <typename T> void Stack<T>::push(T const& elem) { elems.push_back(elem); } ... #endif // stacktest1.cpp // 注意,这里和前面链接器报错的例子不同,这里是包含进了stackdef.hpp, // 这个文件里面含有函数的定义,所以不会产生链接器找不到的错误(其实在编译器中就已经能找到函数模板的定义了) #include "stackdef.hpp" // 书中是“stack.hpp”,应该有误 #include <iostream> #include <string> int main() { Stack<int> intStack; intStack.push(42); } // stack_inst.cpp #include "stack.hpp" // 书中是“stackdef.hpp”,应该有误 #include <string> template Stack<int>; template Stack<std::string>::Stack(); template void Stack<std::string>::push(std::string const&); template std::string Stack<std::string>::top() const;
6.3 分离模型
上面给出的两种方法都可以正常工作,也完全符合C++标准。然而,标准还给出了另一种机制:导出模板。这种机制通常也被称为C++模板的分离模型。
6.3.1 关键字 export
大体上讲,关键字export的功能使用是非常简单的:在一个文件里面定义模板,并在模板的定义和(非定义的)声明的前面加上关键字export。对于上面的例子改写如下:
// basics/myfirst3.hpp #ifndef MYFIRST_HPP #define MYFIRST_HPP // 模板声明 export template <typename T> void print_typeof(T const&); #endif // MYFIRST_HPP
注:
1. 即使在模板定义不可见的条件下,被导出的模板也可以正常使用。换句话说,使用模板的位置和模板定义的位置可以在两个不同的翻译单元。
2. 在一个预处理文件内部(就是指在一个翻译单元内部),我们只需要在第一个声明前面标记export关键字就可以了,后面的重新声明(也包括定义)会隐式地保留这个export特性,这也是我们不需要修改文件myfirst.cpp的原因所在。另一方面,在模板定义中提供一个冗余的export关键字也是可取的,因为这样可以提高代码的可读性。
3. 实际上关键字export可以应用于函数模板、类模板的成员函数、成员函数模板和类模板的静态数据成员。另外,它还可以用于类模板的声明,这将意味着每个可导出的类成员(注意,是可导出的类成员,不可导出的依然不可导出)都被看做可导出实体,但类模板本身实际上却没有被导出(因此,类模板的定义仍然需要出现在头文件中)。你仍然可以隐式或者显式地定义内联成员函数。然而,内联函数却是不可导出的,如下:
export template <typename T> class MyClass { public: void memfun1(); // 被导出的函数 void memfun2(){ ... } // 隐式内联不能被导出 ... void memfun3(); // 显式内联不能被导出 ... }; template <typename T> inline void MyClass<T>::memfun3() // 使用inline关键字,显式内联 { ... }
4. export 关键字不能和inline关键字一起使用;如果用于模板的话,export要位于关键字template的前面,如下:
tempalte <typename T> class Invalid { public: export void wrong(T); // ERROR, export 没有位于template之前 }; export template <typename T> // ERROR,同时使用export和inline inline void Invalid<T>::wrong(T) { ... }
6.3.2 分离模型的限制
1. export特性为能像其他C++特性那样广为流传;
2. export需要系统内部为“模板被实例化的位置和模板定义出现的位置”建立一些我们看不见的耦合;
3. 被导出的模板可能会导致出人意料的语义。
6.3.3 为分离模型做好准备 一个好的办法就是:对于我们预先编写的代码,存在一个可以包含模型和分离模型之间互相切换的开关。我们使用预处理指示符来获得这种特性,如下:
#ifndef MYFIRST_HPP #define MYFIRST_HPP // 如果定义了USE_EXPORT,就使用export #if defined(USE_EXPORT) #define EXPORT export #else #define EXPORT #endif // 模板声明 EXPORT template <typename T> void print_typeof(T const&); // 如果没有定义USE_EXPORT,就包含模板定义 #if !defined(USE_EXPORT) #include "myfirst.cpp" #endif #endif // MYFIRST_HPP
6.4 模板和内联
把短小函数声明为内联函数是提高运行效率所普遍采用的方法。inline修饰符表明的是一种实现:在函数的调用处使用函数体(即内容)直接进行内联替换,它的效率要优于普通函数的调用机制(针对短小函数而言)。然而,标准并没有强制编译器实现这种“在调用处执行内联替换”的机制,实际上,编译器也会根据调用的上下文来决定是否进行替换(内联并不是一种强制执行的机制)。
函数模板和内联函数都可以被定义于多个翻译单元中。通常,我们是通过下面的途径来获取这个实现:把定义放在一个头文件中,而这个头文件又被多个dot-C文件所包含(#include)。
这种实现会给我们这样一个印象:函数模板缺省情况下是内联的。然而,这种想法是不正确的。所以,如果你编写需要被实现为内联函数的函数模板,你仍然应该使用inline修饰符(除非这个函数由于是在类定义的内部进行定义的而已经被隐式内联了)。
因此,对于许多不属于类定义一部分的短小模板函数,你应该使用关键字inline来声明它们。
6.5 预编译头文件
1. 当翻译一个文件时,编译器是从文件的开头一直进行到文件的末端的;
2. 当处理文件中的每个标记(这些标记可能来自于#include的文件)时,编译器会匹配它的内部状态,包括添加入口点到符合表,从而在后面可以查找等。在这个过程中,编译器还会在目标文件中生成代码。
3. 预编译头文件机制主要依赖于下面的事实:我们可以使用某种方式来组织代码,让多个文件中前面的代码都是相同的。充分利用预处理头文件的关键之处在于:(尽可能地)确认许多文件开始处的相同代码的最大行数。这意味着以#include指示符开始,同时意味着包含顺序也相当重要;
4. 通常我们会直接创建一个名为std.hpp的头文件,让它包含所有的标准头文件;
5. 管理预编译头文件的一种可取方法是:对预编译头文件进行分层,即根据头文件的使用频率和稳定性来进行分层。
6.6 调试模板
我们叙述的大多数编译期错误就是由于违反了某些约束而产生的,我们把这些约束称为语法约束;而对于其他约束,我们称为语义约束。concept这个术语通常被用于表示:在模板库中重复需求的约束集合。concept还可以形成体系:就是说,某个concept可以是其他concept的进一步细化(也称为精华,更严格的约束),更精华的concept不但具备上层concept的各种约束,而且还增加了一些针对自身的约束。调试模板代码的主要工作是判断模板实现和模板定义中哪些concept被违反了。
更详细的内容参见书籍。
------------------------------------------------------------------------------------------------------------
第7章 模板术语
7.1 “类模板”还是“模板类”
在C++中,类和联合(union)都被称为类类型(class type)。如果不加额外的限定,我们通常所说的“类(class)”是指:用关键字class或者struct引入的类类型。需要特别注意的一点就是:类类型包括联合,而“类”不包括联合。
7.2 实例化和特化
模板实例化是一个通过使用具体值替换模板实参,从模板产生出普通类、函数或者成员函数的过程。这个过程最后获得的实体(譬如类、函数或成员函数)就是我们通常所说的特化。
然而,在C++中,实例化过程并不是产生特化的唯一方式。程序员可以使用其他机制来显式地指定某个声明,该声明对模板参数进行特定的替换,从而产生特化,如:
template <typename T1, typename T2> // 基本的类模板 class MyClass { ... }; template<> // 显式特化 class MyClass<std::string, float> { ... };
严格地说,上面就是我们通常所讲的显式特化(区别于实例化特化或者其他方式产生的特化)。
template <typename T> // 基本的类模板 class MyClass<T, T> { ... }; template<typename T> // 局部特化 class MyClass<bool, T> { ... };
另外,当谈及(显式或隐式)特化的时候,我们把普通模板称为基本模板。
7.3 声明和定义
1. 声明是一种C++构造,它引入(或重新引入)一个名称到某个C++作用域(scope)中;
2. 另外,对于宏定义和goto语句而言,即使它们都具有一个名称,但它们却不属于声明的范畴;
3. 如果已经确定了这种C++构造(即声明)的细节,或者对于变量而言,已经为它分配了内存空间,那么声明就变成了定义;
4. 对于“类类型或者函数的”定义,这意味着必须提供一对花括号内部的实体;
5. 对于变量而言,进行初始化和不具有extern关键字的声明都是定义。编译器必须基于(哪个)特定实参对所包含的模板定义进行实例化
7.4 一处定义原则(ODR)
“C++语言的定义”在各个实体的重新声明上面强加了一些约束,一处定义原则(或称为ODR,one-definition rule)就是这些约束的全体。基本原则如下:
1. 和全局变量与静态数据成员一样,在整个程序中,非内联函数和成员函数只能被定义一次;
2. 类类型和内联函数在每个翻译单元中最多只能被定义一次,如果存在多个翻译单元,则其所有的定义都必须是等同的。
注:一个翻译单元是指:预处理一个源文件所获得的结果;就是说,它包括#include指示符(即所包含的头文件)所包含的内容。
另外,我们所说的可链接实体指的是下面的实体:非内联函数或者非内联成员函数、全局变量或者静态成员变量,还包括从模板产生的上述这些实体。
7.5 模板实参和模板参数
模板参数:位于模板声明或定义内部,关键字template后面所列举的名称;
模板实参:用来替换模板参数的各个对象。
一个基本原则是:模板实参必须是一个可以在编译期确定的模板实体或者值。如下:
template <typename T> // 模板参数 class Dozen { public: ArrayInClass<T, 12> contents; // 模板实参 };
====================================================================================================================================================================================================================================================================================================================================================================
C++ Template 模版的本质
自动化是人类进化的动力
AlexCool
本文出现的目的,就是尽量让人们理解C++模版设计的思想, 属于模板的心法。
我想知道上帝是如何创造这个世界的。我对这个或那个现象,这个或那个元素的能谱不感兴趣。我要知道的是他的思想。其他都是细节。
——爱因斯坦
模版最初的目的就是为了减少重复代码,所以模版出现的目的就是为了解放C++程序员生产力。
content
C++模版是什么
什么是参数化容器类
什么是通用算法
模板是怎么解决上面问题的
C++实现参数化类(class template)技术
C++实现模板函数(function template)技术
实现C++模板的几个核心技术
模版典型的应用场景有哪些
对模版的展望
更多细节请参考资料和进一步阅读
C++模版是什么?
程序 = 数据结构 + 算法
---Niklaus EmilWirth
最初C++是没有标准库的,任何一门语言的发展都需要标准库的支持,为了让C++更强大,Bjarne Stroustrup觉得需要给C++提供一个标准库,但标准库设计需要一套统一机制来定义各种通用的容器(数据结构)和算法,并且能很好在一起配合,这就需要它们既要相对的独立,又要操作接口保持统一,而且能够很容易被别人使用(用到实际类中),同时又要保证开销尽量小(性能要好)。 Bjarne Stroustrup 提议C++需要一种机制来解决这个问题,所以就催生了模板的产生,最后经标准委员会各路专家讨论和发展,就发展成如今的模版, C++ 第一个正式的标准也加入了模板。
C++模版是一种解决方案,初心是提供参数化容器类和通用的算法(函数)。
什么是参数化容器类?
首先C++是可以提供OOP(面向对象)范式编程的语言,所以支持类概念,类本身就是现实中一类事物的抽象,包括状态和对应的操作,打个比喻,大多数情况下我们谈论汽车,并不是指具体某辆汽车,而是某一类汽车(某个品牌),或者某一类车型的汽车。
所以我们设计汽车这个类的时候,各个汽车品牌的汽车大体框架(骨架)都差不多,都是4个轮子一个方向盘,而且操作基本上都是相同的,否则学车都要根据不同厂商汽车进行学习,所以我们可以用一个类来描述汽车的行为:
class Car
{
public:
Car(...);
//other operations
...
private:
Tire m_tire[4];
Wheel m_wheel;
//other attributes
...
};
但这样设计扩展性不是很好,因为不同的品牌的车,可能方向盘形状不一样,轮胎外观不一样等等。所以要描述这些不同我们可能就会根据不同品牌去设计不同的类,这样类就会变得很多,就会产生下面的问题:
1 代码冗余,会产生视觉复杂性,本身相似的东西比较多;
2 用户很难通过配置去实现一辆车设计,不好定制化一个汽车;
3 如果有其中一个属性有新的变化,就得实现一个新类,扩展代价太大。
这个时候,就希望这个类是可以参数化的(属性参数化),可以根据不同类型的参数进行属性配置,继而生成不同的类。
类模板就应运而生了,类模板就是用来实现参数化的容器类。
什么是通用算法?
程序=数据结构+算法;
算法就是对容器的操作,对数据结构的操作,一般算法设计原则要满足KISS原则,功能尽量单一,尽量通用,才能更好和不同容器配合,有些算法属于控制类算法(比如遍历),还需要和其他算法进行配合,所以需要解决函数参数通用性问题。举个例子:
以前我们实现通用的排序函数可能是这样:
void sort (void* first, void* last, Cmp cmp);
这样的设计有下面一些问题:
1.为了支持多种类型,需要采用void*参数,但是void*参数是一种类型不安全参数,在运行时候需要通过类型转换来访问数据。
2.因为编译器不知道数据类型,那些对void*指针进行偏移操作(算术操作)会非常危险(GNU支持),所以操作会特别小心,这个给实现增加了复杂度。
所以要满足通用(支持各种容器),设计复杂度低,效率高,类型安全的算法,模板函数就应运而生了,模板函数就是用来实现通用算法并满足上面要求。
模板是怎么解决上面问题的?
C++标准委员会采用一套类似函数式语言的语法来设计C++模板,而且设计成图灵完备 (Turing-complete)(详见参考),我们可以把C++模板看成是一种新的语言,而且可以看成是函数式编程语言,只是设计依附在(借助于)C++其他基础语法上(类和函数)。
C++实现参数化类(class template)技术:
1.定义模板类,让每个模板类拥有模板签名。
模板类语法:
template<typename T>
class X{...};
上面的模板签名可以理解成:X<typename T>; 主要包括模板参数<typename T>和模板名字X(类名), 基本的语法可以参考《C++ Templates: The Complete Guide》,《C++ primer》等书籍。
模板参数在形式上主要包括四类:
为什么会存在这些分类,主要是满足不同类对参数化的需求:
type template parameter,类型模板参数,以class或typename 标记;
此类主要是解决朴实的参数化类的问题(上面描述的问题),也是模板设计的初衷。
non-type template parameter,非类型模板参数,比如整型,布尔,枚举,指针,引用等;
此类主要是提供给大小,长度等整型标量参数的控制,其次还提供参数算术运算能力,这些能力结合模板特化为模板提供了初始化值,条件判断,递归循环等能力,这些能力促使模板拥有图灵完备的计算能力。
template template parameter,模板参数是模板;
此类参数需要依赖其他模板参数(作为自己的入参),然后生成新的模板参数,可以用于策略类的设计policy-base class。
parameter pack,C++11的变长模板参数 ;
此类参数是C++11新增的,主要的目的是支持模板参数个数的动态变化,类似函数的变参,但有自己独有语法用于定义和解析(unpack),模板变参主要用于支持参数个数变化的类和函数,比如std::bind,可以绑定不同函数和对应参数,惰性执行,模板变参结合std::tuple就可以实现。
2.在用模板类声明变量的地方,把模板实参(Arguments)(类型)带入模板类,然后按照匹配规则进行匹配,选择最佳匹配模板.
模板实参和形参类似于函数的形参和实参,模板实参只能是在编译时期确定的类型或者常量,C++17支持模板类实参推导。
3.选好模板类之后,编译器会进行模板类实例化--记带入实际参数的类型或者常量自动生成代码,然后再进行通常的编译。
C++实现模板函数(function template)技术:
模板函数实现技术和模板类形式上差不多:
template<typename T>
retType function_name(T t);
其中几个关键点:
函数模板的签名包括模板参数,返回值,函数名,函数参数, cv-qualifier;
函数模板编译顺序大致:名称查找(可能涉及参数依赖查找)->实参推导->模板实参替换(实例化,可能涉及 SFINAE)->函数重载决议->编译;
函数模板可以在实例化时候进行参数推导,必须知道每个模板的实参,但不必指定每个模板的实参。编译器会从函数实参推导缺失的模板实参。这发生在尝试调用函数、取函数模板地址时,和某些其他语境中;
函数模板在进行实例化后(template argument deduction/substitution)会进行函数重载解析(overloading resolution, 此时的函数签名不包括返回值;
函数模板实例化过程中,参数推导不匹配所有的模板或者同时存在多个模板实例满足,或者函数重载决议有歧义等,实例化失败;
为了编译函数模板调用,编译器必须在非模板重载、模板重载和模板重载的特化间决定一个无歧义最佳的模板;
实现C++模板的几个核心技术:
SFINAE -Substitution failure is not an error ;
要理解这句话的关键点是failure和error在模板实例化中意义,模板实例化时候,编译器会用模板实参或者通过模板实参推导出参数类型带入可能的模板集(模板备选集合)中一个一个匹配,找到最优匹配的模板定义,
Failure:在模板集中,单个匹配失败;
Error: 在模板集中,所有的匹配失败;
所以单个匹配失败,不能报错误,只有所有的匹配都失败了才报错误。
2. 模板特化
模板特化为了支持模板类或者模板函数在特定的情况(指明模板的部分参数(偏特化)或者全部参数(完全特化))下特殊实现和优化,而这个机制给与模板某些高阶功能提供了基础,比如模板的递归(提供递归终止条件实现),模板条件判断(提供true或者false 条件实现)等。
3. 模板实参推导
模板实参推导机制给与编译器可以通过实参去反推模板的形参,然后对模板进行实例化,具体推导规则见参考;
4. 模板计算
模板参数支持两大类计算:
一类是类型计算(通过不同的模板参数返回不同的类型),此类计算为构建类型系统提供了基础,也是泛型编程的基础;
一类是整型参数的算术运算, 此类计算提供了模板在实例化时候动态匹配模板的能力;实参通过计算后的结果作为新的实参去匹配特定模板(模板特化)。
5. 模板递归
模板递归是模板元编程的基础,也是C++11变参模板的基础。
模版典型的应用场景有哪些?
C++ Library:
可以实现通用的容器(Containers)和算法(Algorithms),比如STL,Boost等,使用模板技术实现的迭代器(Iterators)和仿函数(Functors)可以很好让容器和算法可以自由搭配和更好的配合;
2 C++ type traits
通过模板技术,C++ type traits实现了一套操作类型特性的系统,C++是静态类型语言,所以需要再编译时候对类型进行检查,这个时候type traits可以提供更多类型信息给编译器,能让程序可以做出更多选择和优化。 C++创始人对traits的理解:
”Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details". - Bjarne Stroustrup
3. Template metaprogramming-TMP
随着模板技术的发展,模板元编程逐渐被人们发掘出来,metaprogramming本意是进行源代码生成的编程(代码生成器),同时也是对编程本身的一种更高级的抽象,好比我们元认知这些概念,就是对学习本身更高级的抽象。 TMP通过模板实现一套“新的语言”(条件,递归,初始化,变量等),由于模板是图灵完备,理论上可以实现任何可计算编程,把本来在运行期实现部分功能可以移到编译期实现,节省运行时开销,比如进行循环展开,量纲分析等。
(取自参考文献6)
4. Policy-Based Class Design
C++ Policy class design 首见于 Andrei Alexandrescu 出版的 《Modern C++ Design》 一书以及他在C/C++ Users Journal杂志专栏 Generic<Programming>,参考wiki。通过把不同策略设计成独立的类,然后通过模板参数对主类进行配置,通常policy-base class design采用继承方式去实现,这要求每个策略在设计的时候要相互独立正交。STL还结合CRTP (Curiously recurring template pattern)等模板技术,实现类似动态多态(虚函数)的静态多态,减少运行开销。
5. Generic Programming
由于模板这种对类型强有力的抽象能力,能让容器和算法更加通用,这一系列的编程手法,慢慢引申出一种新的编程范式:泛型编程。 泛型编程是对类型的抽象接口进行编程,STL库就是泛型编程经典范例。
对模版的展望
模版带来的缺点是什么?
模板的代码和通常的代码比起来,可读性很差,所以很难维护,对人员要求非常高,开发和调试比较麻烦。 对模板代码,实际上很难覆盖所有的测试,所以为了保证代码的健壮性,需要大量高质量的测试,各个平台(编译器)支持力度不一样(模板递归深度,模板特性),可移植性不能完全保证。模板多个实例很有可能会隐式地增加二进制文件的大小等,所以模板在某些情况下有一定代价;
2. 基于模板的设计模式
随着C++模板技术的发展,以及大量的经验总结,逐渐形成了一些基于模板的经典设计,比如STL里面的特性(traits),策略(policy),标签(tag)等技法;Andrei Alexandrescu 提出的Policy-Based Class Design;以及Jim Coplien的curiously recurring template pattern (CRTP),以及衍生Mixin技法;或许未来,基于模板可以衍生更多的设计模式。
3. 模板的未来
C++标准准备引进Concepts(Concepts: The Future of Generic Programming by Bjarne Stroustrup)解决模板调试问题,对模板接口进行约束;随着模板衍生出来的泛型编程,模板元编程,模板函数式编程等理念的发展,将来也许会发展出更抽象,更通用编程理念。模板本身是图灵完备的,所以可以结合C++其他特性:编译期常量和常量表达式,编译期计算,继承,友元friend等开阔出更多优雅的设计,比如元容器,类型擦除,自省和反射等,将来会出现更多设计。
希望本文对想学习C++模板的同学有一定的帮助,有不对的地方还请指正,多谢!
更多细节请参考资料和进一步阅读:
1《The design and Evolution of C++ 》Bjarne Stroustrup;
2. C++ Templates are Turing Complete,Todd L. Veldhuizen,2003(作者网站已经停了,archive.org 保存的版本,archive.org 可能被限制浏览);
3. 模板细节:
wikipedia.org, cppreference.com(C++,模板template, Template metaprogramming, CRTP (Curiously recurring template pattern), Substitution failure is not an error (SFINAE), template_argument_deduction ,Policy-based_class design, Expression templates,等);
C++ Programming/Templates/Template Meta-Programming
4.模板的基本语法:
C++标准ISO+IEC+14882-1998,2003,2011;
《C++ Templates: The Complete Guide》 by David Vandevoorde, Nicolai M. Josuttis;
5.模板设计进价:
Andrei Alexandrescu 的 《Modern C++ Design》;
候捷的《STL源码剖析》;
More C++ Idioms:wikipedia.org
6. 模板元编程:
Abrahams, David; Gurtovoy, Aleksey. C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond. Addison-Wesley. ISBN 0-321-22725-5.
Metaprogramming in C++,Johannes Koskinen,2004
Boost 的MPL库
7.Blog and papers:
Coplien, James O. (February 1995). "Curiously Recurring Template Patterns" (PDF). C++ Report: 24–27.
https://en.wikipedia.org/wiki/Mixin
http://www.cnblogs.com/liangliangh/p/4219879.html
http://b.atch.se/posts/non-constant-constant-expressions/
---------------------
作者:_AlexCool_
来源:CSDN
原文:https://blog.csdn.net/lianhunqianr1/article/details/79966911
版权声明:本文为博主原创文章,转载请附上博文链接!
=============================================================================================================================================================================================================================================================================================================================================================================================================================================================
C++中 模板Template的使用
1、在c++Template中非常多地方都用到了typename与class这两个关键字,并且好像能够替换,是不是这两个关键字全然一样呢?
答:class用于定义类,在模板引入c++后,最初定义模板的方法为:template,这里class关键字表明T是一个类型。后来为了避免class在这两个地方的使用可能给人带来混淆,所以引入了typename这个关键字。它的作用同class一样表明后面的符号为一个类型。这样在定义模板的时候就能够使用以下的方式了: template.在模板定义语法中关键字class与typename的作用全然一样。
2.类模板与模板类的概念
(1) 什么是类模板
一个类模板(也称为类属类或类生成类)同意用户为类定义一种模式。使得类中的某些数据成员、默写成员函数的參数、某些成员函数的返回值,能够取随意类型(包含系统提前定义的和用户自己定义的)。
假设一个类中数据成员的数据类型不能确定。或者是某个成员函数的參数或返回值的类型不能确定。就必须将此类声明为模板,它的存在不是代表一个详细的、实际的类,而是代表着一类类。
(2)类模板定义
定义一个类模板,一般有双方面的内容:
A.首先要定义类,其格式为:
template <class T>
class foo
{
……
}
foo 为类名,在类定义体中,如採用通用数据类型的成员,函数參数的前面需加上T。当中通用类型T能够作为普通成员变量的类型,还能够作为const和static成员变量以及成员函数的參数和返回类型之用。
比如:
template<class T>
class Test{
private:
T n;
const T i;
static T cnt;
public:
Test():i(0){}
Test(T k);
~Test(){}
void print();
T operator+(T x);
};
B. 在类定义体外定义成员函数时,若此成员函数中有模板參数存在,则除了须要和一般类的体外定义成员函数一样的定义外,还需在函数体外进行模板声明
比如
template<class T>
void Test<T>::print(){
std::cout<<"n="<<n<<std::endl;
std::cout<<"i="<<i<<std::endl;
std::cout<<"cnt="<<cnt<<std::endl;
}
假设函数是以通用类型为返回类型,则要在函数名前的类名后缀上“”。
比如:
template<class T>
Test<T>::Test(T k):i(k){n=k;cnt++;}
template<class T>
T Test<T>::operator+(T x){
return n + x;
}
C. 在类定义体外初始化const成员和static成员变量的做法和普通类体外初始化const成员和static成员变量的做法基本上是一样的,唯一的差别是需再对模板进行声明,比如
template<class T>
int Test<T>::cnt=0;
template<class T>
Test<T>::Test(T k):i(k){n=k;cnt++;}
(3) 类模板的使用 类模板的使用实际上是将类模板实例化成一个详细的类。它的格式为:类名<实际的类型>。
模板类是类模板实例化后的一个产物。说个形象点的样例吧。
我把类模板比作一个做饼干同的模子,而模板类就是用这个模子做出来的饼干,至于这个饼干是什么味道的就要看你自己在实例化时用的是什么材料了,你能够做巧克力饼干,也能够做豆沙饼干,这些饼干的除了材料不一样外,其它的东西都是一样的了。
3.函数模板和模板函数
(1)函数模板
函数模板能够用来创建一个通用的函数。以支持多种不同的形參。避免重载函数的函数体反复设计。
它的最大特点是把函数使用的数据类型作为參数。
函数模板的声明形式为:
template<typename(或class) T>
<返回类型><函数名>(參数表)
{
函数体
}
当中,template是定义模板函数的关键字;template后面的尖括号不能省略;typename(或class)是声明数据类型參数标识符的关键字。用以说明它后面的标识符是数据类型标识符。这样,在以后定义的这个函数中,凡希望依据实參数据类型来确定数据类型的变量,都能够用数据类型參数标识符来说明,从而使这个变量能够适应不同的数据类型。
比如:
template<typename(或class) T>
T fuc(T x, T y)
{
T x;
//……
}
函数模板仅仅是声明了一个函数的描写叙述即模板。不是一个能够直接运行的函数,仅仅有依据实际情况用实參的数据类型取代类型參数标识符之后,才干产生真正的函数。
(2)模板函数:
模板函数的生成就是将函数模板的类型形參实例化的过程。
比如:
double d;
int a;
fuc(d,a);
则系统将用实參d的数据类型double去取代函数模板中的T生成函数:
double fuc(double x,int y)
{
double x;
//……
}