因为之前看boost源码在模板上遇到了比较大的问题,所以前段时间抽空读了《c++templates》,而且网上缺少成体系的博客,所以在这里和大家分享下。这算是一个系统性的学习笔记,希望帮助到大家。
多态和泛型谈到模板大家一定听说过泛型编程或者元编程。那么我们一定会思考什么是泛型,它和多态的区别是什么?
- 首先多态和泛型都目的都是为了实现多型,只是两种实现的方式不一样。
- 多态的实现我们在c++中是通过基类指针指向子类对象来实现的,通过virtual关键字将函数推断推迟到运行时,在运行时,通过查找虚函数表来调用不同子类的成员函数来实现多型。
- 泛型(模板)是编译器通过传入的类型参数帮我们生成对应类型的代码,以达到模板类对不同类型有不同表现的目的。模板根据类型实例化对应类型代码,是编译期以及link时确定的,所以模板成员函数无法用virtual修饰。
- 模板相对于多态最大的有点就是,模板是编译期确定函数调用,不需要在运行过程中查找虚函数表,运行效率更高。模板缺点是:会增大代码体积,以及模板报错往往发生在链接时,难排查问题。
步骤是先模板类, 模板函数 。下面命名不规范。
模板类 定义第一个模板类知识点:
- 类内定义成员函数
- 类外定义成员函数
template使用 接下来我们把container_ 类型参数化class MyStack1 { public: // 类内定义 void Push(const T &element) { container_.push_back(element); }; T Pop(); private: std::vector container_; }; // 类外定义 template T MyStack1 ::Pop() { if (container_.size() == 0) { return T(); } T result = container_.back(); container_.pop_back(); return result; };
template使用 模板 模板类型参数class MyStack2 { public: void Push(const T &element) { container_.push_back(element); }; T Pop() { if (container_.size() == 0) { return T(); } T result = container_.back(); container_.pop_back(); return result; } private: CONT container_; };
有了上面的使用方式,我们会思考能否吧vector< int > 省去。
我们可以使用template template moudule param
- MyStack3第二个类型参数前加 template是告诉编译器第二个类型参数为模板类
- typename = std::allocator 等于 typename [类型别名] = std::allocator,如果类型别名用不上,则可以省略不写。
- template template 模板参数需要和传入模板类的模板参数列表保持严格一致。
为使用方便我们可以给一个模板参数设置默认类型
代码template使用 非类型模板参数?> typename CONT = std::vector> class MyStack4 { public: void Push(const T &element) { container_.push_back(element); }; T Pop() { if (container_.size() == 0) { return T(); } T result = container_.back(); container_.pop_back(); return result; } private: CONT container_; };
如果我们想预设一个大小呢?
模板参数列表不仅仅可以传类型,也可以传无类型(non-type)参数如int,枚举(这里最常用的为int 以及enum,double,float不可以,感兴趣可以自行了解下)
修改代码部分如下
到了这里我们基本上就掌握了类模板的最基本的用法。接下来我们要掌握模板类的特化(包括偏特化)。看这一部分时我们可以把特化版本当作实现多态的子类,把primary模板类当作基类。我们可以通过特化使特化版本生成不同于primary模板类规定的逻辑版本。全特化和偏特化说白了就是本该让编译器生成的代码由程序员完成。
- 特化T为CLASS1的版本
- 特化T为CLASS1的版本,container_类型为deque的版本
根据结果我们还发现了偏特化版本匹配的机制,和重载函数一样优先选择匹配度高的
class CLASS1
{
};
template >
typename CONT = std::vector>
class MyStack5
{
public:
MyStack5()
{
container_.resize(SIZE);
};
public:
void Push(const T &element)
{
container_.push_back(element);
};
T Pop()
{
if (container_.size() == 0)
{
return T();
}
T result = container_.back();
container_.pop_back();
return result;
}
private:
CONT container_;
};
// // 偏特化二:当模板参数1为CLASS1
template >
typename CONT>
class MyStack5
{
public:
MyStack5()
{
container_.resize(SIZE);
};
public:
void Push(const CLASS1 &element)
{
std::cout << "class1 Push" << std::endl;
};
CLASS1 Pop()
{
std::cout << "class1 Pop" << std::endl;
};
private:
CONT container_;
};
// 偏特化二:当模板参数1为CLASS1 ,模板参数二 为 std::deque时走下面逻辑
template
class MyStack5
{
public:
MyStack5()
{
container_.resize(SIZE);
};
public:
void Push(const CLASS1 &element)
{
std::cout << "class1 Push2" << std::endl;
};
CLASS1 Pop()
{
std::cout << "class1 Pop2" << std::endl;
};
private:
std::deque container_;
};
模板类继承
最后在这里补充下部分继承相关的问题
模板类 继承 模板类,子类使用父类函数时需要明确该函数是父类的,否则将会在子类的命名空间中查找该函数。模板类继承非模板类则没有上述情况。
完整代码在下面了,建议自行尝试下。
template模板函数class Base { public: void exit() { std::cout << "base foo" << std::endl; } }; // class Base // { // public: // void exit() // { // std::cout << "base foo" << std::endl; // } // }; template class Derive : public Base { public: void foo1() { // exit(); Base ::exit(); } };
如果上面一步步过来,一定可以较为优雅的实现模板类了,下面我们来学习模板函数。
模板函数首要记住的一点就是,模板函数不支持默认类型参数(因为可以用函数重载实现)。
template使用 总结RT Myfunc(T1 arg1, T2 arg2){ }; template RT Myfunc1(T1 arg1, T2 arg2){ };
0值问题
- 模板函数的模板参数可以通过传入参数推断;也可以通过手动确定,如(2)手动确认所有类型参数;
- RT 类型无法通过传入参数列表推断,所以需要手动确定。
- (1)处我们希望int做为RT类型,T1,T2由传入参数推断,但结果编译不通过。原因:因为编译器把int当场了我们为T1手动确认的类型,T2由传入参数2确定为int,这样一来RT就没有被实例化,所以编译报错。
所以我们可以把RT放在模板类型参数列表的第一个,如(3)这样即可依次确认RT,T1,T2类型,编译通过。
有了上面的基础,我们从这里正式开始完善模板函数的学习。
[注]Accumulate 左闭右开求和。
看到下面这段代码,一定会有同学指出 T sum没有赋初值的问题。是的,如果T为拥有默认构造函数的类,编译器会帮助我们调用默认构造函数初始化,但int这种内置类型则无法这样做(为了兼容c标准)。
T sum = 0 这样写明显不正确。所以这时诞生处了零值问题。
template0值问题解决T Accumulate(const T *begin, const T *end) { T sum; //注意这有bug ,当T为int时 sum为垃圾值 while (begin != end) { sum += *begin; // std::cout << *begin << std::endl; ++begin; //++i占用寄存器较少,相比i++更快些 } return sum; }
1)改写为 T sum = T() 即可解决。c++中int 会被赋值为0,bool会被赋值为false。
templateT Accumulate(const T *begin, const T *end) { T sum = T(); while (begin != end) { sum += *begin; // std::cout << *begin << std::endl; ++begin; //++i占用寄存器较少,相比i++更快些 } return sum; }
2) 上面那种做法需要有默认构造函数,那如果某个类没有默认构造函数呢?
template使用 :class AccumulateTrait1 { }; template <> class AccumulateTrait1 { public: typedef int ACCT; static int zero() { return 0; }; }; template <> class AccumulateTrait1 { public: typedef double ACCT; static double zero() { return 0.0; }; }; template //用typename 告诉编译器AccumulateTrait1 ::ACCT是类型,而不是静态数据成员或者枚举 typename AccumulateTrait1 ::ACCT Accumulate1(const T *begin, const T *end) { T sum = AccumulateTrait1 ::zero(); while (begin != end) { sum += *begin; // std::cout << *begin << std::endl; ++begin; //++i占用寄存器较少,相比i++更快些 } return sum; };
我们可以借助其他模板类 AccumulateTrait1特化,处理那些没有默认构造函数的类。至此,我们已经完成了一个挺完善的Accumulate1模板函数。
给模板类添加默认类型参数。上文已经提到模板函数不支持默认参数,但我们可以用模板类+静态函数来为某个模板函数添加默认参数。
如下:我们将吹毛求疵,我们给用户确定让Accumulate返回值类型的权限。
template::ACCT> class ACCMU1 { public: static RT Accumulate1(const T *begin, const T *end) { RT sum = AccumulateTrait1 ::zero(); while (begin != end) { sum += *begin; // std::cout << *begin << std::endl; ++begin; //++i占用寄存器较少,相比i++更快些 } return sum; }; }; template typename AccumulateTrait1 ::ACCT Accumulate2(const T *begin, const T *end) { return ACCMU1 ::Accumulate1(begin, end); } template typename Trait::ACCT Accumulate2(const T *begin, const T *end) { return ACCMU1 ::Accumulate1(begin, end); }
使用
总结,相比上一步而言,这种方法缺点是:没有办法进行模板参数推断了,优点是:可以让客户更灵活的使用。
为了向上一步一样使用,我们只好使用函数再封装一层封装,并且利用函数重载简化使用。
template策略模式typename AccumulateTrait1 ::Acct accumlate1(const T *begin, const T *end) { return Accum ::Accumlate1(begin, end); } // 可以用用户自定以的type trait template typename Trait::Acct Accumlate1(const T *begin, const T *end) { return Accum ::Accumlate1(begin, end); }
模板实现策略模式
接下来我们 我们使用模板来实现策略模式,依旧来继续完善我们的Accumlate,让Accumlate可以替换累积策略。
方法一// 策略1
class SimpleSumPolicy
{
public:
template
static void Sum(T &total, const T &b)
{
total += b;
};
};
//策略2
class SpecialSumPolicy
{
public:
template
static void Sum(T &total, const T &b)
{
total += 2 * b;
};
};
template ::ACCT>
class ACCMU2
{
public:
static RT Accumulate3(const T *begin, const T *end)
{
RT sum = AccumulateTrait1::zero();
while (begin != end)
{
Policy::Sum(sum, *begin);
++begin;
}
return sum;
};
};
方法二
我们可以使用模板类+偏特化实现。该方法优点暂时没想到,所以我个人目前推荐方法一,因为足够简单。希望大家可以指点下,谢谢。
template模板函数形参类型 根据模板类型参数选择引用传值或者按值传值class SumPolicy2 { public: static void accumulate(T1 &totol, const T2 &value) { totol += value; } }; template class Policy = SumPolicy2, //这样就强制要求sumpolicy有两个模板参数 typename Trait = AccumulateTrait > class Accum2 { public: typedef typename Trait::Acct Acct; static Acct Accumlate3(const T *begin, const T *end) { Acct sum = Trait::Zero(); while (begin != end) { Policy ::accumulate(sum, *begin); ++begin; } return sum; } };
传入参数我们能否让它根据不同类型传不同的值,比如:int,bool等按值拷贝,因为内置类型拷贝开销很小,反而寄存器间接寻址开销更大;class 则传引用(const 引用同理)。为了更符合语义,另起一个sum函数做讲解。
为达成目的,我们首先要学会判断是否为class。为了判断是否为class我们需要知道什么是SFINE以及类成员指针
SFINE简单来说,就是当模板函数类型显示确定(手动)时,类型推断失败时,不会生成该版本实例,而不会编译报错。这部分网上文章很多,我也没有完全体会过官网例子,就不误人子弟了。
SFINE官方文档
类成员指针
该类作用判断一个类型是类还是其他。
代码如下
templateclass IsClass1 { private: typedef char one; typedef struct func_template { char a[2]; } two; template static one test(int C::*);//传入参数为C的类成员指针 template static two test(...);//...标识传入参数为除C的类成员指针以外的其他情况 public: enum { YES = sizeof(IsClass1 ::test (0)) == sizeof(one) }; enum { NO = !YES }; };
注意点:
- 枚举是属于类的,不是属于对象的,所以它的初始化不能调用成员函数,所以必须将两个test声明为静态函数。
- 有人可能会对这IsClass1::test(0)感到疑惑.
IsClass1::test(0)其实就是调用test,可是明明我们没定义test函数啊?模板函数如果仅声明但没定义,在实例化时依旧会为我们生成默认版本(建议自行声明一个模板,调用下试试)。接下来就是匹配问题了,T用int实例化时,int不属于class的范畴,所以没有类成员指针,static one test(int C:)该版本生成失败,所以编译器选择test(…)版本,最终yes = sizeof(two) == sizeof(one)。
下面是完整代码:没有什么新东西看自行看注释
//自定义类型
class TestClass
{
public:
TestClass() = default;
TestClass &operator=(const TestClass &other){
};
TestClass &operator+(const TestClass &other)
{
}
};
//类型trait
template
class AccumulateTrait1
{
};
template <>
class AccumulateTrait1
{
public:
typedef int ACCT;
static int zero()
{
return 0;
};
};
template <>
class AccumulateTrait1
{
public:
typedef TestClass ACCT;
static TestClass zero()
{
return TestClass();
};
};
template <>
class AccumulateTrait1
{
public:
typedef double ACCT;
static double zero()
{
return 0.0;
};
};
//两个策略类
class SimpleSumPolicy
{
public:
template
static void Sum(T &total, const T &b)
{
total += b;
};
};
class SpecialSumPolicy
{
public:
template
static void Sum(T &total, const T &b)
{
total += 2 * b;
};
};
//用于判断是否为class
template
class IsClass1
{
private:
typedef char one;
typedef struct func_template
{
char a[2];
} two;
template
static one test(int C::*);
template
static two test(...);
public:
enum
{
YES = sizeof(IsClass1::test(0)) == sizeof(one)
};
enum
{
NO = !YES
};
};
// 类型选择辅助类 non类型参数为true 选择第一个类型,false选择第二个类型
template
class IfThen1;
template
class IfThen1
{
public:
typedef T1 ResultT;
};
template
class IfThen1
{
public:
typedef T2 ResultT;
};
//萃取提升,利用偏特化根据T提供不同的类型
template
class TypeOp1
{
public:
typedef T Type;
typedef T &REFT;
typedef const T CONST;
};
template
class TypeOp1
{
public:
typedef const T Type;
typedef const T &REFT;
typedef const T CONST;
};
template
class TypeOp1
{
public:
typedef T &Type;
typedef T &REFT;
typedef const T CONST;
};
//参数类,根据类型推断对应的传入参数类型
template
class Param1
{
public:
typedef typename IfThen1::YES,
typename TypeOp1::REFT,
typename TypeOp1::Type>::ResultT Result;
};
//sum函数
template ::ACCT>
class ACCMU3
{
public:
static RT Sum(typename Param1::Result begin, typename Param1::Result end)
{
RT sum = begin + end;
return sum;
};
};
使用
可以看到当为int时 Sum传值方式为按值传参
可以看到当为TestClass时 Sum传值方式为按引用传参(当然你也可以选择传const 引用)
1)是赋值运算符支持隐式转换。我个人不推荐,因为我觉得明确的参数传递更加严谨,我们不应该为使用简单而增加犯错的风险,下面是代码,感兴趣的可以研究下。
templateclass MyBaseContainer { public: //参数列表初始化signature_ 避免int不初始化的问题 MyBaseContainer() : signature_() { } protected: void start() { std::cout << "MyBaseContainer start" << std::endl; }; private: T signature_; }; // 模板组织方式选择置入式(即只有头文件,缺点是会导致包含过多文件,导致编译速率的降低) // class CONT 强调CONT为模板类 template > // template template parameter 需要严格匹配vector的参数列表 class CONT = std::vector> class MyContainer : public MyBaseContainer { public: void operator=(const MyContainer &ops) { std::cout << "operation = default" << std::endl; } // void operator=(const MyContainer &ops) = delete;会报错说明 下面的=无法替代默认=运算符 template > // template template parameter 需要严格匹配vector的参数列表 class CONT2 = std::vector> MyContainer &operator=(const MyContainer &ops); public: void Start() { MyBaseContainer ::start(); //模板继承体系必须加上域解析符,确定start为基类中的,而不是子中的 } inline bool Empty() { return contaner_.empty(); } inline void Push(const T &element) { contaner_.push_back(element); } inline T Pop() { assert(!contaner_.empty()); T result = contaner_.front(); std::cout << "Pop : " << result << std::endl; contaner_.pop_back(); return result; } void Traverse() const { // std::cout << contaner_[0] << std::endl; // std::cout << contaner_[1] << std::endl; for (int i = 0; i < contaner_.size(); ++i) { std::cout << contaner_[i] << std::endl; } // for (T a : contaner_) // { // std::cout << a << std::endl; // } // std::cout << std::endl; } private: CONT contaner_; }; // 支持可以类型隐式转化的进行赋值 template // template template parameter 需要严格匹配vector的参数列表 class CONT> template // template template parameter 需要严格匹配vector的参数列表 class CONT2> MyContainer & MyContainer ::operator=(const MyContainer &ops) { std::cout << "tempalet operation =" << std::endl; // 两个指针类型不一致 擦除指针类型 if ((void *)this == (void *)&ops) { return *this; } MyContainer tmp(ops); contaner_.clear(); while (!tmp.Empty()) { std::vector a; a.push_back(1.0); const T2 element = tmp.Pop(); std::cout << "operator= : element is : " << element << std::endl; contaner_.push_back(element); // ops.Pop(); } return *this; }
- 模板类中的模板【template的受控名称】。这一部分,在boost.asio中就有用到,可以注意下下。介于本人对编译器处理模板还不是足够了解,就不做过度解读了,但帮大家测试出了正确写法如下。
在一个模板类中定义一个模板
templateclass Country { public: template class Province { public: static void func1(){}; template static void func2(T3 arg1){}; private: T a; }; template static void countryFunc(T3 a){ }; typedef int Type; };
使用
- 在普通函数中使用
void testMain()
{
// typename Country::Province a; //报错,编译器可以推断出Province为模板类,会认为重复定义了
// Country::Province a; //可见编译器可以知道Province是一个模板类
// Country::Type b; //报错,编译器认为Type是静态成员
// typename Country::Type b;
Country::Province::func2(1); //可见编译器可以知道func2是一个模板函数类
Country::Province::func2(1); //可见编译器可以知道func2是一个模板函数类
Country::template Province::func2(1);//可见编译器可以知道func2是一个模板函数类
}
- 在另一个类中使用
template变长类型包class WorldMap { public: static void func(T1 a) { T2 c; // typename Country ::Province a; //会报错,会把Province当做非模板处理,需要我们用template明确告诉编译器是模板类 typename Country ::template Province b; // typename 告诉编译器Province 为一个类型 // Country ::Province ::func1(); //编译报错,编译器知道后面的<>为模板参数列表,需要我们用template明确告诉编译器是模板类 // Country ::Province ::func2(a); // 编译报错, 编译器知道后面的<>为模板参数列表,需要我们用template明确告诉编译器是模板类 Country ::template Province ::func1(); Country ::template Province ::func2(a); Country ::template Province ::func2(c); // Country ::template Province ::func2 (a);编译报错,编译器不会解析后面的模板参数列表,需要我们用template明确告诉编译器是模板类 Country ::template Province ::template func2(a); Country ::template Province ::template func2 (a); }; };
推荐一个简明扼要的博客。
https://www.cnblogs.com/qicosmos/p/4325949.html



