条款四十一:了解隐式接口的和编译期多态
和面向对象编程的显示接口和运行期多态不同,
泛型编程更多是隐式接口和编译期多态。
#include <iostream>
using namespace std;
class A
{
public:
A() { x = 12; cout << "Create A" << endl; }
virtual std::size_t size() const { return x; }
virtual void Init() { x = 10; cout << "Init A" << endl; }
void swap(A& other) { int t = x; x = other.x; other.x = t; cout << "Swap" << endl; }
bool operator != (const int & x)
{
cout << "!=" << endl;
return this->x != x;
}
private:
int x;
};
template<class T>
void Func(T& a)
{
if (a.size() > 10 && a != 5)
{
T temp(a);
temp.Init();
temp.swap(a);
}
}
int main()
{
A a;
Func(a);
return 0;
}
/*
Create A
!=
Init A
Swap
*/
1.以不同的参数具现化会导致调用不同的函数,这便是所谓的编译期多态。
2.泛型编程必须支持对应的隐式转换,如果函数中使用到诸如,size(),Init(),swap(),!=
请记住:
classes和templates都支持接口和多态。
对classes而言接口是显示的,以签名函数为中心。多态则是virtual函数发生于运行期。
对template参数而言接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
条款四十二:了解typename的双重意义
template<class T>
template<typename T>
没什么不同,但是c++并不总是把class和typename视为等价。
涉及嵌套从属名称时只能使用关键字typename:
(从属名称(dependent name):template内出现的名称(示例中的iter)如果相依于某个template参数(示例中的C)的名称。如果从属名称在class内呈嵌套状,则称为嵌套从属名称(nested dependent name)。const_iterator也是类。
)
#include <iostream>
using namespace std;
class A
{
public:
typedef int const_iterator;
private:
int x;
};
template<typename T>
void Func(T& x)
{
T::const_iterator* x;
}
int main()
{
A a;
Func(a);
return 0;
}
这段代码编译会有报错
为什么?
因为我们不知道T::const_iterator是不是一个类型。我们无法确定!
可能T的某个静态成员变量恰好叫const_iterator,或者x碰巧是个global变量名称呢?
那样的话上述代码甚至不再是声明一个local变量而是一个相动作!!!
(T::const_iterator)* (x)!!!
所以c++规定必须使用typename为前缀。
#include <iostream>
using namespace std;
class A
{
public:
typedef int const_iterator;
private:
int x;
};
template<typename T>
void Func(T& a)
{
typename T::const_iterator* x;
}
int main()
{
A a;
Func(a);
return 0;
}
任何时候当你想要在template中指涉一个从属类型名称,就必须在紧临它的前一个位置放上关键字typename。
但是有例外!typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class 修饰符。
#include <iostream>
using namespace std;
template<typename T>
class Base
{
public:
class Nested
{
public:
Nested(int x) { cout << x << endl; }
};
};
template<typename T>
class Dierived :public Base<T>::Nested
{
public:
Dierived(int x) :Base<T>::Nested(x) {}
int x;
};
int main()
{
Dierived<int> d(10);
return 0;
}
/*
输出:10
*/
即不能写成
typename Base::Nested
对于这种长名称我们可以通过typedef简写,这是非常常用的技巧,谁不想少写点单词呢?
template<typename T>
void Func(T& a)
{
typedef typename T::const_iterator* iter;
iter x;
}
请记住:
声明template参数时,前缀关键字class和tyoename可互换
请使用typename标识嵌套从属类型名称:但不得在base class lists(基类列)和member initalization list(成员初值列)内以它作为base class修饰符
条款四十三:学习处理模板化基类内的名称
看这一段代码。Send(info)无法通过编译
#include <iostream>
using namespace std;
class CompanyA
{
public:
void Send(string msg)
{
cout << "A Send : " << msg << endl;
}
};
class CompanyB
{
void Send(string msg)
{
cout << "B Send : " << msg << endl;
}
};
class MsgInfo
{
public:
string msg;
};
template<typename Company>
class MsgSender
{
void Send(const MsgInfo& info)
{
string msg;
msg = info.msg;
Company c;
c.Send(msg);
}
};
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void SendLog(const MsgInfo& info)
{
//log
Send(info);//编译器不知道Company是什么,所以无法知道MsgSender<Company>是什么
}
};
int main()
{
return 0;
}
问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道他继承的是什么样的class。主要是不知道Company是什么。更明确的说是无法确定有Send这个函数。
这涉及到模板特化的问题!
我们看一个例子,假设我们引入了一个模板全特化的MsgSender
class CompanyZ
{
};
template<>
//一个全特化的MsgSender
class MsgSender<CompanyZ>
//,它和一般template相同,差别只是没有Send
{
};
所谓的模板全特化:template MsgSender针对类型Companyz特化了,并且其特化是全面的,也就是说一旦类型参数被定义为Companyz,再没有其他template参数可供变化。
(说人话:模板参数列表中all的模板参数都用具体的类型标识)
当类模板/函数模板进行全特化之后,这个全特化后的类/函数就不是一个模板类/函数了
(因为全特化完成后,模板参数类型为空了,即template<>了,就是一个具体的类/函数)
那么我们再考虑之前的LoggingMsgSender
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void SendLog(const MsgInfo& info)
{
//log
Send(info);//如果Company==Companyz这个函数不存在
}
};
这就是为什么C++拒绝调用的原因:它知道base class template可能被特化,而那个特化版本可能不提供和一般性template相同的接口。
解决方案有三种:
1.加上“this->”
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void SendLog(const MsgInfo& info)
{
//log
this->Send(info);
}
};
2.使用using声明式
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
using MsgSender<Company>::Send;
void SendLog(const MsgInfo& info)
{
//log
Send(info);
}
};
3.明白指出被调用的函数位于base class内:
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{
public:
void SendLog(const MsgInfo& info)
{
//log
MsgSender<Company>::Send(info);
}
};
请记住:
可在derived class templates内通过”this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class 资格修饰符”完成。
条款四十四:将与参数无关的代码抽离templates
使用template可能导致代码膨胀,二进制码会带着重复(或者几乎重复)的代码、数据,或两者。其结果有可能源码看起来合身而整齐,但目标码却不是这么回事。所以该条例用于我们解决template带来的代码膨胀问题。
#include <iostream>
using namespace std;
template <class T>
class SquareMatrixBase
{
public:
SquareMatrixBase(T* p) : DataPointer(p) {}
void Invert(size_t n) {}//求逆矩阵
private:
T* DataPointer;
};
template <class T, size_t n>
class SquareMatrix:private SquareMatrixBase<T> //n*n的矩阵T类型的
{
public:
SquareMatrix() : SquareMatrixBase<T>(Data)
{}
void Invert()
{
SquareMatrixBase<T>::Invert(n);//调父类函数
}
private:
T Data[n * n];
};
int main()
{
SquareMatrix<int, 10> a;
SquareMatrix<int, 5> b;
}
请记住:
Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
条款四十五:运用成员函数模板接受所有兼容类型
真实的指针考研支持隐式转换。但我们自己简单写的指针不能这样
#include <iostream>
using namespace std;
template <class T>
class SmartPtr
{
public:
SmartPtr(T* p) : p(p) {}
private:
T* p;
};
class A
{
};
class B :public A
{
};
int main()
{
SmartPtr<A> p1=SmartPtr<B>(new B);//不行
shared_ptr<A> p2 = shared_ptr<B>(new B);//可以
}
为什么?如果以带有base-derived关系的A和B类分别具现化某个template,产生出来的两个具现化并不带有base-derived的关系,所以编译器认为这是两个完全无关的类。
解决方案是生成一个member template copy构造函数
#include <iostream>
using namespace std;
template <class T>
class SmartPtr
{
public:
template<typename U>
SmartPtr(const SmartPtr<U> & other):p(static_cast<T*>(other.get())) {
}
SmartPtr(T* p) : p(p) {}
T* get() const
{
return p;
}
private:
T* p;
};
class A
{
};
class B :public A
{
};
int main()
{
SmartPtr<A> p1=SmartPtr<B>(new B);//不行
shared_ptr<A> p2 = shared_ptr<B>(new B);//可以
}
请记住:
请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数;
如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
条款四十六:需要类型转换时请为模板定义非成员函数
为什么唯有non-menber函数才有能力”在所有实参身上实施隐式类型转换”。
#include <iostream>
using namespace std;
template <typename T>
class A {
public:
A(T t) {
this->t = t;
}
A(const A<T>& e) {
this->t = e.t;
}
const A<T>& operator=(const A<T>& e) {
this->t = e.t;
return *this;
}
template<typename T>
const A<T> operator*(const A<T>& t1, const A<T>& t2) {
return A<T>(t1.t*t2.t);
}
private:
T t;
};
int main() {
A<int> t1(10);
A<int> t2 = t1 * 10;//报错
return 0;
}
上述失败的原因是编译器不知道我们想要调用哪个函数,它们试图想出什么函数名为operator*的template具现化。但它们必须要知道T是什么,它们没有这个能耐。
第一个参数t1是A类型的,但第二个参数是int类型,那么编译器如何推算出T?
只需要:template class内的friend声明式可以指涉某个特定函数。那意味class A可以声明operator*为friend函数。所以编译器总是能够在class A具现化时得知T。
#include <iostream>
using namespace std;
template <typename T>
class A {
public:
A(T t) {
this->t = t;
}
A(const A<T>& e) {
this->t = e.t;
}
const A<T>& operator=(const A<T>& e) {
this->t = e.t;
return *this;
}
friend const A<T> operator*(const A<T>& t1, const A<T>& t2) {
return A<T>(t1.t*t2.t);
}
private:
T t;
};
int main() {
A<int> t1(10);
A<int> t2 = t1 * 10;//没报错
return 0;
}
请记住:
当我们编写一个class temlate,而它所提供之“与此template相关的”函数支持”所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”
条款四十七:请使用traits classes 表现类型信息
完全看不懂。
主要是搞不明白traits。大概好像是利用模板偏特化使得模板得到类型?
traits
模板与泛型
请记住:
Traits classes使得”类型相关信息”在编译期可用。它们以templates和“templates特化完成实现”。
整合重载技术后,traits classes有可能在编译期对类型执行if。。else测试
条款四十八:认识template元编程
Template metaprogramming(TMP 模板元编程)
它有两个伟大效力:
第一,它让某些事情更容易。
第二,由于TMP执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期转移到编译期的另一个结果是,编译时间变长了。
好的,我们来写hello world==》阶乘
#include <iostream>
using namespace std;
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n - 1>::value };
};
//特化模板,Factorial<0>的value值为1
template<>
struct Factorial<0> {
enum { value = 1 };
};
int main() {
Factorial<5> a;
cout << a.value << endl;
return 0;
}
有了这个TMP,只要你指涉Factorial::value就可以得到n阶乘值。循环发生在template具现体内部指涉另一个template具现体Factional之时。我们需要一个特殊情况造成递归结束,这里的特殊情况是template特体化Factorial<0>。
此外,为了领悟TMP之所以值得学习,很重要的一点是先对它能够达成什么目标有一个比较好的理解,下面是三个例子:
第一、确保度量单位正确。如果使用TMP,就可以确保(在编译器)程序中所有量度单位的组合都正确,不论其计算多么复杂。这也是为什么TMP可被用来进行早期错误侦测。
第二,可以优化矩阵运算。如果使用高级、与TMP相关的template技术,即所谓的expression templates,就有可能消除多个矩阵相乘时产生的临时对象并合并循环。
第三,可以生成客户定制的设计模式实现品。运用所谓policy-based design的TMP-based技术,有可能产生一些templates用来表述独立的设计选项,然后可以任意结合他们,导致模式实现品带着客户定制的行为。此外也可以用来避免生成对某些特殊类型并不合适的代码。
请记住:
TMP可将工作由运行期转移到编译期,因而得以实现早期错误侦测或者更高的执行效率。
TMP可被用来生成“基于政策选择组合”的客户定制代码,也可以用来避免生成对某些特殊类型并不适合的代码。(这句话看不懂也没关系)