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