条款41 了解隐式接口和编译期多态
模板编程是一种隐式接口编译期多态技术,class则是显示接口,运行时多态
- 对于class而言接口是显示的,以函数签名为中心,多态则是通过virtual函数发生在运行期。
- 对于template而言,接口是隐式的,多态通过template具现化和函数重载解析发生在编译器
条款42 了解typename的双重意义
在模板中class和typename关键字没有区别,模板内的嵌套从属类型名称默认是被当成函数,只有在前面添加typename才会被当成一种类型。但是typename无法在成员初始化列表中作为base class修饰符,也无法出现在基类列表中。
条款43 学习处理模板化基类内的名称
class CompanyA {
public:
void sendCleartext(const std::string& msg);
void sendEcrypted(const std::string& msg);
}
class CompanyB {
public:
void sendCleartext(const std::string& msg);
void sendEcrypted(const std::string& msg);
}
class MsgInfo{....};
template<typename Company>
class MsgSender {
public:
void sendClear(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{....}
};
上面这段代码看上去,工作良好,也的确如此。根据不同的Company,发送不同的消息。现在需要在发送消息的前后添加
一些日志,于是有了下面的这个派生类
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
....
void sendClearMsg(const MsgInfo& info)
{
//发送前些写log
sendClear(info);
//发送后写log
}
};
为了避免名称遮掩,子类使用了另外一个名字。但是很可惜上面的代码无法编译通过,因为在调用sendClear的时候,
会报不存在的错误。很明显这个函数是在base class中的,是存在的,为什么这里会报不存在的错误呢。现在假想一种
场景,base class会被特化,很可能是一种完全特化的版本,那么这会导致base class没有sendClear这个接口,
因此C++就明确的拒绝这种行为,不去base class中查找指定的方法。
为了解决上面提出的这个问题,C++中有三个方法可以解决,方法如下:
- 明确使用this
this->sendClear(info);
- using声明
public:
....
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info)
{
//发送前些写log
sendClear(info);
//发送后写log
}
- 明确指出sendClear在base类中
MsgSender<Company>::sendClear(info);
但是上面的这个方法,存在一个问题,当sendClear是个虚函数的时候,会关闭virtual绑定行为。
条款45 运用成员函数模板接收所有兼容类型
C/C++语言中的指针做的很好的一件事就是支持隐式转换,Derived class指针可以隐式转换为base class指针,指向non-const对象的指针可以转换为指向const对象等等,下面是一个他们之间转换的例子:
class Top {....};
class Middle: public Top {...};
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pct2 = pt1;
上面的这种隐式转换得益于指针,但是不幸的是在C++中智能指针并不能自动去实现这种隐式转换,智能指针本身就是利用template去实现的,同一个template的不同具现体之间并不存在什么与生俱来的固有关系。例如下面的代码
template<typename T>
class SmartPtr {
public:
explicit SmartPtr<T* realPtr>;
....
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Middle>(new Bottom);
SmartPtr<const Top> pct2 = pt1;
上面的代码无法编译通过, 所有Top和Middle有父子关系,如果是指针是可以隐式转换的,但是同一个template的不同具现体之间并不存在什么与生俱来的固有关系。 因此为了可以让上面的代码可以编译通过,需要给SmartPtr类提供兼容
- 请使用member function templates(成员函数模板)生成,可接受所有兼容类型的函数
- 如果你声明member function用于”泛化copy构造”或”泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符
条款46 需要类型转换时请为模板定义非成员函数
只有non-member函数才有能力在所有的实参身上实施隐式类型转换,下面的例子演示了模板函数无法对所有实参实施隐式类型转换的场景。
template<typename T>
class Rational {
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)
{ ...... }
上面的乘法操作符使用了模板来实现,为了可以对任意实参都具备隐式类型转换的能力,所以采用了non-member函数,具体细节可以参考条款24,尽管如此下面的代码仍然无法通过编译。
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2;
尽管可以通过oneHalf推导出第一个实参的类型是int,但是通过第二个参数无法推导出其类型,因为template函数在实参推导过程中从不进行隐式类型转换。为了解决这个问题可以通过在Rational加上一个friend函数声明即可解决问题,代码如下。
template<typename T>
class Rational {
public:
.....
friend const Rational operator*(const Rational& lhs,
const Rational& rhs);
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs);
当Rational具现化后,对应的友元函数operator*
也会具现化一份,那么就变成了一个non-member函数了,此时就可以做隐式类型转换了。尽管如此,上面的代码在链接阶段却无法链接,原因在于我们通过模板具现化在类的内部产生了一个operator*
的友元函数声明,但是还没有其定义,我们期望可以通过类外的operator*
函数模板为我们提供其定义,但是这行不通。为了解决这个问题,需要将operator*
的定义式写在类的内部。将声明和定义放在一起。现在就可以正常编译和链接了。
- 当我们编写一个class template,而它所提供之,”与此template相关的”函数支持,”所有参数之隐式类型转换”时,请将那么函数定义为
class template内部的friend函数。
条款47 请使用traits classes表现类型信息
简而言之就是在编译期获得类型信息,一般结合template function来完成针对不同的类型采取不同的操作这一功能。如果没有traits的话,那么这个template function会通过typeid得到类型信息,然后根据类型信息采取不同的操作,很显然typeid得到类型信息是一个运行期操作。因此有了traits classes,可以在编译期间获得类型信息。例如STL算法库中的advance算法,用于移动迭代器,对于某些迭代器只能每次移动一步,而对于随机迭代器可以一次移动N步,因此advance算法需要根据传入的迭代器类型采取不同的算法。为此我们需要给advance 提供一个traits classes。其实就是所有的自定义类型都提供一个typedef将自己的迭代器类型都通过typedef进行名称统一,然后traits classes再次通过typedef给这些自定义类型提供一个统一的名称,就这样就实现了编译器得到类型信息这个目的。但是advance还可以接收指针这种内置类型,我们没法给修改指针,让指针提供一个typedef,因此可以给advance提供一个针对指针的特化版本的实现。
- Traits classes使得”类型相关信息”在编译期可用,他们以templates和templates特化完成实现。
- 整合重载技术后traits classes有可能在编译器对类型指向if..else测试