关于C++模板函数的一种简化代理
什么是模板
C++相较于C的一大特点除了OOP,便是它的模板化(template)功能。这也是别的语言(除了C#以外)所没有的优势。
什么是模板?简而言之它是一种预处理功能(pre-processing function),类似宏(Macro),不过比后者有着无比强大的优势。当然在C++编程里面,宏的地位也是不可替代的,而且宏经常可以和模板一起使用达到神乎其神的效果(例如boost)。
模板有具体分为模板函数(template function)和模板类(template class),这里主要是要讨论一下关于模板函数的一些问题。
为什么需要是模板
例如上一篇关于类型转换函数中的例子:
int sum(int valueA, int valueB); float sum(float valueA, float valueB);
我们可以针对float和int来实现一个求和的函数,但是如果我们有很多种类型都想实现呢?要是为每一种类型都去写一个实现必然繁琐而且会产生冗余,而冗余很容易带来更新丢失。
也许有人就想那就只实现一个double的,反正别的数据类型都能往double转,这是一个看似通用但却是极其危险的做法。且不说编译器会给你报告无数的警告(warning),而且在高安全级别的编译环境下这些警告都会被直接提升为错误(error)。而解决这些问题这正是模板函数的功能作用所在。
怎么使用模板
模板函数都会以template关键字开头,后面跟着用尖括号(<>)引起来的类型参数列表:
template<typename ValueType>
ValueType sum(ValueType valueA, ValueType valueB)
{
return valueA + valueB;
}
这个函数便是一个模板函数,它“目前”还只是一个不存在的函数,因为你还没使用过它,要使用它也很简单:
int intA, intB; float floatA, floatB; int intC = sum(intA, intB); float floatC = sum(floatA, floatB);
这个时候,编译器会检查到你尝试使用sum这个模板函数,并且自动为你展开为两个版本:ValueType为int和float的两个版本,也就是说相当于编译器自动帮你实现了第一个例子中的那两个sum函数。
使用模板函数的好处
- 编译器自动展开,可以保证每个版本的sum内部实现一致,避免了冗余。
- 模板函数里面的类型可以通过传入参数自动推导,所以不需要担心某些类型忘记了实现对应版本。
- 因为是模板是有静态(编译期)类型验证的,所以产生的函数都是类型安全的。
指定模板类型
当然模板函数也有使用起来不太方便的地方,比较常见的一点就是当编译器不能推导模板参数类型的时候,需要用户手动指定,例如:
int intA; float floatB; float floatC = sum(intA, floatB);
这个时候因为传入的参数有两种不同的类型,所以编译器不能决定到底匹配那个版本的sum函数,便会导致编译失败。这个问题可以通过手动指定模板参数类型来解决,例如:
int intA; float floatB; float floatC = sum<float>(intA, floatB);
这时编译器会尝试用float版本的sum函数进行匹配,当然可能会产生int到float的隐式类型转换警告。
除了上面那种情况外还有一些情况用户必须手动指定模板类型,比如当函数没有参数或者参数类型始终一样,但是需要返回值类型不一样的时候。
举个例子,如果你想实现一种通用的属性管理容器,在其中能保存任何类型,然后你希望通过一个字符串作为索引,例如:
class PropertyContainer
{
public:
template<typename ValueType>
ValueType GetValue(const char* pKey);
... ...
}
PropertyContainer pc;
bool boolValue = pc.GetValue<bool>("FooBoolValue");
char charValue = pc.GetValue<char>("FooCharValue");
int intValue = pc.GetValue<int>("FooIntValue");
float floatValue = pc.GetValue<float>("FooFloatValue");
这样使用虽然可以达到取出想要的数值的功能,但是却很不方便的每次都得指定模板类型,因为编译器不可能通过返回值的类型来推断(返回值不能决定函数原型)。
通过代理简化模板的调用
然后同样也是废话完毕,进入重点。
为了避免每次都指定模板类型,可以通过一种简单的代理方式来实现。首先将上面的GetValue函数改造为:
class PropertyContainer;
class PropertyProxy
{
public:
PropertyProxy(PropertyContainer* pContainer, const char* pKey)
: m_pContainer(pContainer)
, m_pKey(pKey)
{}
template<typename ValueType>
operator ValueType()
{
return m_pContainer->GetValueImpl<ValueType>(m_pKey);
}
private:
PropertyContainer* m_pContainer;
const char* m_pKey;
}
class PropertyContainer
{
public:
PropertyProxy GetValue(const char* pKey)
{
return PropertyProxy(this, pKey);
}
template<typename ValueType>
ValueType GetValueImpl(const char* pKey);
... ...
}
PropertyContainer pc;
bool boolValue = pc.GetValue("FooBoolValue");
char charValue = pc.GetValue("FooCharValue");
int intValue = pc.GetValue("FooIntValue");
float floatValue = pc.GetValue("FooFloatValue");
这时函数PropertyContainer::GetValue已经不再是一个模板函数(相应的模板函数由PropertyContainer::GetValueImpl替代),它返回的不是直接的结果而是一个统一代理类型的对象,然后将这个对象赋值给目标变量。这时PropertyProxy中的模板类型转换运算符会自动的根据想要转换的目标类型进行在展开,并且自动的在内部调用对应模板类型版本的PropertyContainer::GetValueImpl函数,从而使得调用方便简单而且还能做到类型安全。