Effective C++ 第七章—— 模板与泛型编程
条款41——了解隐式接口和编译器多态
隐式接口(implicit interface):对于template而言所必须有效编译的一组表达式,就是类型T必须支持的一组隐式接口;
编译器多态(compile-time polymorphism):以不同的template参数具现化的function templates 会导致调用不同的函数。
通常显示接口由函数的签名式(函数名称、参数类型、返回类型)构成。而隐式接口并不相同,它不基于签名式,而是由有效的表达式组成。
例如:
template<typename T>
void doProcessing(T &w)
{
if(w.size() > 10 && w != someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
doProcessing函数中,T必须提供一个名为size的函数,size只需要返回一个X类型的对象,而X对象加上一个int类型必须能够调用operator>;T还需要支持operator != ,operator !=接受一个X类型和Y类型的对象,只要T类型可以被转换为X类型,someNastyWidget类型可以被转换为Y类型。其他隐式接口:copy构造函数、normalize和swap函数也都必须对T类型有效。
请记住:
- class 和template都支持接口(interface)和多态(polymorphism);
- 对class而言接口都是显示的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期;
- 对于template参数而言,接口都是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
条款42——了解template的双重意思
在template声明式中,class和typename没有什么不同。
- 从属名称:template内出现的名称如果相依于某个template参数,则称为从属名称(dependent names);
- 嵌套从属名称:从属名称在class内呈嵌套状。
- 谓非从属名称:一个不依赖template参数的名称。
例如:
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
C::const_iterator iter(container.begin());//C::const_iterator是嵌套从属名称
++iter;
int vlaue = *iter;//int是谓非从属名称
std::cout<<value;
}
}
编译器开始解析时,如果遇到一个嵌套从属名称,这是编译器假设他不是一个类型,除非你告诉他是。
给出一个一般性规则解决上述问题,任何时候当你想要在template中涉及一个嵌套从属类型名称,就必须在它的前一个为止上放置关键字typename;这一规则的例外是typename不可以出现在base class list内的嵌套从属类型名称之前,也不可以出现在membei initialization list(成员初值列)中作为base class修饰符。例如:
template<typename T>
class Derived: public Base<T>::Nested { //base class list中不允许“typename”
public:
explicit Derived(int x): Base<T>:: Nested(x)//成员初值列不允许“typename”
{
typename Base<T>::Nested temp;//嵌套从属类型名称,既不在base class list也不在
//mem.init.list中,作为一个base class修饰符需要加上typename
...
}
...
};
请记住:
- 声明template参数时,前缀关键字class和typename可互换;
- 请使用关键字typename标识嵌套类从属类型名称但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
条款43——学习处理模板化基类内的名称
当我们定义了一个模板化的base class时,在定义一个模板化的derived class后,在derived class中调用base class的成员函数时,编译器是无法通过的;这是由于当编译器遇到derived class定义式时,并不知道它继承了什么样的class,不到后来(当derived class被具现化)无法确切知道他是什么。往往拒绝在模板化基类(templatized base classes)内寻找继承而来的名称。
class CompanyA{//公司A
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
class CompanyB{//公司B
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
...//针对其他公司的class
class MsgInfo {...};
template<typename Company>
class MsgSender {
public:
... //
void sendClear(const MsgInfo& info)
{
std::string msg;
在这里根据info产生信息;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info){...}
};
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
sendClear(info);
将“传送前”的信息写至log;
}
...
};
为了让C++“不进入templatized base classes 的行为失效”有三种解决办法:
- 在base class函数调用之前加上“this->”;
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
this->sendClear(info);
将“传送前”的信息写至log;
}
...
};
- 用using声明式;
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear;
...
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
sendClear(info);
将“传送前”的信息写至log;
}
...
};
- 明确指出被调用函数位于哪个base class内;这是最不让人满意的一个做法,如果被调用的是virtual函数,上述明确指定资格修饰会关闭virtual的动态绑定行为。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
MsgSender<Company>::sendClear(info);
将“传送前”的信息写至log;
}
...
};
请记住:
- 可在derived class templates内通过“this->”指涉base class template内的成员名称;或藉由一个明白写出的“base class资格修饰符”完成。
条款44——将参数无关的代码抽离template
请记住:
- Template生成多个class和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系;
- 因非类型参数模板而造成的代码膨胀,往往可降低,往往可以消除,做法是以函数或class成员变量替换template参数。
- 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现代码。
条款45——运用成员函数模板接受所有兼容类型
成员函数模板(member function templates)的作用是用于使class模板兼容所有对象,通常用于为构造函数写一个模板,或者支持赋值操作。
同一个template的不同具现体之间并不存在什么与生俱来的关系。即使是一个模板由base class和derived class分别具现化,这两个具现化之间也是没有什么关系的。如果我们希望一个模板由base class和derived class具现化后,能够由derived class具现化的模板转换为base class具现化模板,需要定义一个成员函数模板。这里有时候也称为泛化copy构造函数。
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other)//这里采用成员初始化列表是为了,当存在某个隐式转换可以将一个
//U*指针转换为一个T*指针才能通过编译。
:heldPtr(other.get()) {...};
T* get() {return heldPtr;}
...
private:
T* heldPtr;
};
成员函数模板(member function templates)支持赋值操作,例如
template<typename T>
class shared_ptr {
public:
template<class Y> //构造来自任何兼容的
exlicit shared_ptr(Y* p); //内置指针、
template<class Y>
shared_ptr(shared_ptr<Y> const& t);//shared_ptr、
template<class Y>
exlicit shared_ptr(weak_ptr<Y> const& t); //weak_ptr、
template<class Y>
exlicit shared_ptr(auto_ptr<Y> & t);//auto_ptr;
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r);//赋值,来自任何兼容的shared_ptr、
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r); //auto_ptr
请记住:
- 请使用 member function templates (成员函数模板) 生成“可接受所有兼容类型”的函数;
- 如果你声明member template 用于“泛化copy构造”或“泛化assgnment操作”,你还需要声明正常的copy构造函数和copy assignment操作符。
条款46——需要类型转换是请为模板定义非成员函数
为了使用一个函数模板,我们需要在调用他之前先推导出参数类型,然后将函数进行适当的具现化。但是在template实参推导过程中是不考虑采纳“通过构造函数而发生的”隐式类型转换的 。
template<typename T>
class Rational{
public:
Rational (const T& numerator = 0,const T& denominator = 1);
const T numerator() const;
const T denominator() const;
...
};
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{
...
}
Rational<int> onehalf(1,2);
Rational<int> result = onehalf*2;//错误,无法通过编译!无法推断出operator *的参数类型。
为了解决上述template函数支持所有参数的隐式类型转换,可以在template class内定义friend函数,并在template class内定义它,这样才能正常的编译并执行,如果不在class内定义则只能编译通过但无法连接。为了使inline所带来的冲击最小,如果inline的函数代码块太大的话,会影响程序的效率,这时我们可以令inline后的函数调用辅助函数,缓解压力。
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs);
template<typename T>
class Rational{
public:
...
friend const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{ return doMultiply(lhs,rhs); }
...
};
请记住:
- 当我们编写一个class template, 而它所提供之“与此template相关的” 函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
条款47——请使用traits class 表现类型信息
traits:一种技术,将它放进一个template及一个或多个特化版本中,一种template的应用。它要求对内置类型(主要是指针)和用户自定义类型表现一样好。
当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同(而我们又不希望因为数据类型的差异而修改算法本身的封装时),traits会是一种很好的解决方案。
这样的template在标准库中有多个,其中针对迭代器者被命名为iterator_traits:
template<typename IterT>
struct iterator_traits
{
typedef typename IterT::iterator_category iterator_category;
...
};
//为了支持指针迭代器,iterator_traits针对指针提供一个偏特化版本。
template<typename IterT>
struct iterator_traits<IterT*>
{
typedef random_access_iteraotr_tag iterator_category;
...
};
设计一个traits class的方法
- 确认若干你希望将来可取得的类型相关信息。对迭代器而言,我们希望可以取得其分类。
- 为该信息选择一个名称(例如iterator_category)
- 提供一个template和一组特化版本,内涵你希望支持的类型的相关信息。
//定义一个class方法compute,
//需要在T为int类型时,Compute方法的参数为int,返回类型也为int;
//当T为float时,Compute方法的参数为float,返回类型为int
//而当T为其他类型,Compute方法的参数为T,返回类型也为T,怎么做呢?还是用traits的方式思考下。
template <typename T>
struct TraitsHelper {
typedef T ret_type;
typedef T par_type;
};
template <>
struct TraitsHelper<int> {
typedef int ret_type;
typedef int par_type;
};
template <>
struct TraitsHelper<float> {
typedef int ret_type;
typedef float par_type;
};
//class定义
template <typename T>
class Test {
public:
TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);
private:
T mData;
};
使用traits class的方法
- 建立一组重载函数(身份像劳工)或函数模板,彼此之间的差异只在于各自的traits参数,令每个函数实现码与其接受的traits信息相应和;
- 建立一个控制函数(身份像工头)或函数模板,它调用上述那些劳工函数并传递traits class所提供的信息。
请记住: - Traits classes 使得“类型相关信息”在编译期可用。它们以tempalte 和“template特化”完成实现;
- 整合重载技术(overloading)后,traits classes 有可能在编译期对类型执行if…else测试。
条款48——认识template元编程
Template metaprogramming(TMP ,模板元编程):编写template-based C++程序并执行与编译期的过程。
一个TMP的例子:
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> {
enum { value = 1 };
};
int main()
{
std::cout<<Factorial<5>::value;//打印120;
std::cout<<Factorial<10>::value;//打印3628800
return 0;
}
请记住:
- Template metaprogramming(TMP,模板元编程)可以将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率;
- TMP可被用来生成“基于政策选择组合”的客户定制代码,也可以用来避免生成对某些特殊类型并不合适的代码。