面向对象和多态
面向对象最基本的特性就是多态,用相同的代码得到不同的结果。
以前面讲过的shape为例
class shape {
public:
…
virtual void draw(const position&) = 0;//纯虚函数
};
virtual =0,是纯虚函数的定义,定义了纯虚函数就意味着所有继承shape的子类都必须实现纯虚函数draw。我们可以认为纯虚函数是一个接口(接口是java中的定义),在面向对象的设计中,接口抽象了一些基本的行为,其子类(实现类)则必须去具体实现接口类(父类)定义的接口。
可以通过接口类的指针或引用去调用具体的实现类里的逻辑代码。(就是多态 父类指针指向子类对象,调用子类成员函数)
以shape为例,在一个绘图程序里,我们可以把用户选择的具体形状(子类)赋值给一个shape(父类)的智能指针,在用户点击画板时调用shape的draw函数(实现了多态调用)。
这种面向对象的方式(父类指针指向子类对象)并不是实现多态的唯一方式,
如果一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那么这只鸟就可以被当作鸭子。在java中可以使用反射,不继承shape去实现具体的子类,而是直接调用对象的draw方法,如果对象没有draw就得到异常。
鸭子类型使得开发者可以不使用继承体系从而灵活地实现一些“约定”,尤其是使得混合不同来源、使用不同对象继承体系的代码成为可能。唯一的要求只是,这些不同的对象有“共通”的成员函数。这些成员函数应当有相同的名字和相同结构的参数(并不要求参数类型相同)。
容器的共性
容器最普遍的共性是有begin和end成员函数,这使得用通用的方式遍历容器成为可能。容器不必继承同一个共同的container父类,但我们依旧可以写出通用的遍历容器的代码。(这就是前面说的鸟像鸭子,鸟就可以被当作鸭子)。大部分容器都有size成员,在泛型编程中,我们同样可以用通用的方式得到容器的size而不需要继承同一个szieable_container父类(这就是前面说的鸟像鸭子,鸟就可以被当作鸭子)。很多容器都有push_back成员函数,可以在尾部插入数据,在泛型编程中,我们同样可以用通用的方式push_back元素而不需要继承同一个backpushable_container父类(这就是前面说的鸟像鸭子,鸟就可以被当作鸭子)。
c++标准容器没有对象继承关系,但彼此之间有很多的同构性。这些同构性很难用继承体系表达,c++的模板就已经足够表达这些鸭子类型。
c++模板
1定义模板
下面是一个求最大公约数的算法
int my_gcd(int a, int b)
{
while (b != 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
//因为类型不止一种所以可以用模板表示类型
template <typename E>
E my_gcd(E a, E b)
{
while (b != E(0)) {
E r = a % b;
a = b;
b = r;
}
return a;
}
代码中就是把int替换为模板参数E,并在函数开头添加了模板的声明
2 实例化模板
不管是类模板还是函数模板,编译器在看到其定义时只能做最基本的语法检测,真正的类型检查是在实例化时(也是编译器报错的时候)。
如果成功的话,模板的实例就产生了。在整个的编译过程中,可能产生多个这样的(相同)实例(某种相同的类型的函数/类实例),但最后链接时,会只剩下一个实例。这也是为什么C++ 会有一个单一定义的规则:如果不是单一定义,不同的编译单元(可能时在编译不同文件时)看到不同的定义的话,那链接时使用哪个定义是不确定的,结果就可能会让人吃惊。
模板还可以显式实例化和外部实例化。如果我们在调用 my_gcd 之前进行显式实例化模板——即,即在调用前使用 template 关键字并给出完整的类型来声明函数:
template cln::cl_I
my_gcd(cln::cl_I, cln::cl_I);
extern template cln::cl_I
my_gcd(cln::cl_I, cln::cl_I);
声明时在前面加上extern,编译器会认为这个模板已经在其他某个地方实例化,从而不再产生其定义(但内联函数 在extern声明下仍可能会导致实例化的发生,这个与编译器及其优化选项有关)。
类似的,当我们在使用vector
我们同样可以使用 template class vector
或使用 extern template class vector
显式实例化和外部实例化通常在大型项目中可以用来集中模板的实例化,从而加速编译过程。(不需要在每个用到模板的地方都进行实例化了,相当于只在一个文件中显示实例化一个模板,在其他用到的文件中都声明外部实例化,这样的话只会在 那唯一一次显示实例化模板时,去进行类型检查。 这大大减少编译器类型检查的次数) 但是这种方式有额外的管理开销,并且如果集中显示实例化了不必要实例化的模板的话,反而会导致可执行文件变大。因而,显式实例化和外部实例化应当谨慎使用。
补充函数在头文件内的特化
函数如果不inline的话,正常情况下它的实现不能放头文件里,否则就会有多个定义;但函数模板编译器同样会特殊处理。最后链接后只有一个实例化的结果存在。(头文件内不要写函数的定义,一旦这样做,且头文件被多个其他文件使用 就会导致出现多份函数定义,导致编译链接失败)
不管是否inline,所有编译单元看到的定义必须是完全相同的(不能重复多次定义同个函数或类),否则,仍然可能会有意外发生。
但是如果有场景需要,一定要在头文件内实现特化版本的函数 有3个选项
为特化函数 inline或者extern或者 static
template<>
inline int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
对于大多数模板库而言,这是最容易和最常见的解决方案。因为编译器直接扩展内联函数,不产生外部符号,在多个模块中 #include 它们没有什么问题。链接器不会出错,因为不存在多重定义的符号。并且对于像 compare 这样的小函数来说,inline 声明还会让函数更快。
也可以用extern声明,在头文件内特化了模板但是添加extern告诉编译器这个特化版本的定义在其他地方。 同样也实现了头文件内特化函数模板,当让需要在某个地方真正实现这个特化版本函数的定义
template<>
extern int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2);//外部声明
使用静态static 方式在头文件内定义函数的特化版本。这样链接器也不会出错,因为静态函数不向外界输出其函数,并且它让你将所有东西都保持在一个头文件中,不用引入预处理符号。但它缺乏效率,因为每个模块都有一个函数拷贝。如果函数小到没什么——那为何不用内联呢?
template<>
static int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
所以简言之:将特化做成 inline 或 extern。通常都是用 inline。两种方法都得编辑头文件。如果使用的是第三方的库没有头文件,那么你除了用链接选项 /FORCE:MULTIPLE 之外别无选择。在你等着生成你的工程时,你可以告诉编写库文件的那个家伙——为什么要将函数模板特化定义成 inline 或者 extern。
3特化模板
以 my_gcd为例,在函数中需要用到%,但是我们想要实例化的类型cln::cl_I并没有重载%op。
这种时候有3中方法
(1) 添加代码,让类型支持我们需要的操作
( 一般而言,不应该去修改别人的类。容易出问题。所以不能添加成员函数)
添加cln::cl_I的%op重载函数
cln::cl_I
operator%(const cln::cl_I& lhs,
const cln::cl_I& rhs)
{
return mod(lhs, rhs);
}
在这个例子,这可能是最简单的解决方案了。但在很多情况下,尤其是对对象的成员函数有要求的情况下,这个方法不可行。
(2) 针对cln::cl_I 对特殊的操作 做单独的重载函数版本
template <typename E>
E my_gcd(E a, E b)
{
while (b != E(0)) {
E r = my_mod(a, b);//因为cln::cl_I没有op%所以不用% 而用一个函数代替
a = b;
b = r;
}
return a;
}
//对与一般的类型
template <typename E>
E my_mod(const E& lhs,
const E& rhs)
{
return lhs % rhs;
}
//针对cl_I类做重载(注意下面的这种实现 不是模板特化 而是函数重载)
cln::cl_I
my_mod(const cln::cl_I& lhs,
const cln::cl_I& rhs)
{
return mod(lhs, rhs);
}
(3) 针对cln::cl_I进行模板特化
template <>
cln::cl_I my_mod<cln::cl_I>(
const cln::cl_I& lhs,
const cln::cl_I& rhs)
{
return mod(lhs, rhs);
}
这个例子比较简单,特化和重载在行为上没有本质的区别。就一般而言,特化是一种更通用的技巧,最主要的原因是特化可以用在类模板和函数模板上,而重载只能用于函数。
通用而言,Herb Sutter 给出了明确的建议:对函数使用重载,对类模板进行特化。 当函数重载和特化版本同时满足要求时,重载比特化优先,编译器优先调用重载版本(函数模板特化 有大坑!!!http://www.gotw.ca/publications/mill17.htm))
//////大坑说明
template <class T> // //(a) 基本模板
void f(T);
template <class T> // 重载(a)f函数(b) --- 虽然函数模板没有偏特化的概念,但在这里重载和偏特化很像
void f(T*);
//现在有一个(c) int*类型的全特化版本
template <> //(c) 全特化
void f <>(int *);
//如果我你们将c放在a后面,那c就是对a模板的全特化
template <class T>
void f(T);
template <> //(c) 全特化 对a模板的全特化
void f <>(int *);
template <class T>
void f(T*);// b在最后
//如果我你们将c放在b后面,那c就是对b模板的全特化
template <class T>
void f(T);
template <class T>
void f(T*);
template <> //(c) 全特化 对b模板的全特化
void f <>(int *);
所以int*全特化放置的位置不同,导致了模板裁决时,因为选取了不同的基础模板而导致了不同的结果。建议对于函数直接进行参数类型不同的函数重载,而不要用模板特化以避免这个大坑
//////大坑说明
展示特化的更好的例子是 C++11 之前的静态断言。使用特化技巧可以大致实现 static_assert 的功能:
//下面这个例子也是对模板参数可以为存在的类型 而非必须是typename T这种未知类型
//模板参数可以是未知类型,也可以是常数表达式,包括整数类型常数、枚举、指针、引用。
//声明了一个struct 模板
template <bool>
struct compile_time_error;
//仅对模板参数为true的情况进行特化
template <>
struct compile_time_error<true> {};
#define STATIC_ASSERT(Expr, Msg) \
{ \
compile_time_error<bool(Expr)> \
ERROR_##_Msg; \
(void)ERROR_##_Msg; \
}
//(void)这行的作用是抑制编译器的变量未使用告警,我使用变量并做强转就是使用这个变量,
//并且不会有什么其他后果,同时因为使用了就不会有警告了。
//这是常见的显式告诉编译器不要对变量未使用进行告警的方式。
接下来如果bool(Expr)为fasle即compile_time_error
“动态”多态和”静态”多态的对比
前面讲的 父类指针指向子类对象 是运行时的动态多态
而c++里基于泛型编程的多态 是静态多态(编译时就实例化 确定下来)
两者解决的实际问题是不太一样的
动态多态为了解决运行时行为的变化,就比如选择了一个形状之后,再选择在某个地方绘制这个形状——这个是无法在编译时确定的(形状是用户使用的时候选择)。
静态”多态或者“泛型”——解决的是很不同的问题,让适用于不同类型的“同构”算法可以用同一套代码来实现,实际上强调的是对代码的复用。 c++std种的有很多标准算法都是这样,只做基本的约定 只要满足约定的类型 那实例化后就可以工作。以sort为例,要求3点参数满足随机访问迭代器的要求。迭代器指向的对象之间可以使用 < 来比较大小,满足严格弱序关系。迭代器指向的对象可以被移动。它的性能超出 C 的 qsort,因为编译器可以内联(inline)对象的比较操作;而在 C 里面比较只能通过一个额外的函数调用来实现。此外,C 的 qsort 函数要求数组指向的内容是可按比特复制的,C++ 的 sort 则要求迭代器指向的内容是可移动的,可适用于更广的情况。
补充类模板的全特化和偏特化
例1
#include <iostream>
using namespace std;
template <class T>
class Compare
{
public:
bool IsEqual(const T& arg, const T& arg1);
};
//全特化compare的float版本
// 已经不具有template的意思了,已经明确为float了
template <>
class Compare<float>
{
public:
bool IsEqual(const float& arg, const float& arg1);
};
//全特化compare的double版本
// 已经不具有template的意思了,已经明确为double了
template <>
class Compare<double>
{
public:
bool IsEqual(const double& arg, const double& arg1);
};
//定义泛型 类成员函数IsEqual的实现
template <class T>
bool Compare<T>::IsEqual(const T& arg, const T& arg1)
{
cout<<"Call Compare<T>::IsEqual"<<endl;
return (arg == arg1);
}
//定义float特化 类成员函数IsEqual的实现
bool Compare<float>::IsEqual(const float& arg, const float& arg1)
{
cout<<"Call Compare<float>::IsEqual"<<endl;
return (abs(arg - arg1) < 10e-3);
}
//定义double特化 类成员函数IsEqual的实现
bool Compare<double>::IsEqual(const double& arg, const double& arg1)
{
cout<<"Call Compare<double>::IsEqual"<<endl;
return (abs(arg - arg1) < 10e-6);
}
int main()
{
Compare<int> obj;
Compare<float> obj1;
Compare<double> obj2;
cout<<obj.IsEqual(2, 2)<<endl;
cout<<obj1.IsEqual(2.003, 2.002)<<endl;
cout<<obj2.IsEqual(3.000002, 3.0000021)<<endl;
}
例2
所谓的偏特化是指提供另一份template定义式,而其本身仍为templatized;也就是说,针对template参数更进一步的条件限制所设计出来的一个特化版本就是偏特化。
template <class _Iterator>
struct iterator_traits
{
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};
// 迭代器属性的 指针偏特化版本 还可以进一步特化_Tp类型 所以这是个偏特化
template <class _Tp>
struct iterator_traits<_Tp*>
{
typedef random_access_iterator_tag iterator_category;
typedef _Tp value_type;
typedef ptrdiff_t difference_type;
typedef _Tp* pointer;
typedef _Tp& reference;
};
// 迭代器属性的 常指针偏特化版本 还可以进一步特化_Tp类型 所以这是个偏特化
template <class _Tp>
struct iterator_traits<const _Tp*>
{
typedef random_access_iterator_tag iterator_category;
typedef _Tp value_type;
typedef ptrdiff_t difference_type;
typedef const _Tp* pointer;
typedef const _Tp& reference;
};
例3 还是偏特化的例子
#include <iostream>
using namespace std;
// 一般化设计
template <class T, class T1>
class TestClass
{
public:
TestClass()
{
cout<<"T, T1"<<endl;
}
};
// 针对普通指针的偏特化设计
// T指针偏特化版本 还可以进一步特化T类型 所以这是个偏特化
template <class T, class T1>
class TestClass<T*, T1*>
{
public:
TestClass()
{
cout<<"T*, T1*"<<endl;
}
};
// 针对const指针的偏特化设计
// T常指针偏特化版本 还可以进一步特化T类型 所以这是个偏特化
template <class T, class T1>
class TestClass<const T*, T1*>
{
public:
TestClass()
{
cout<<"const T*, T1*"<<endl;
}
};
int main()
{
TestClass<int, char> obj;
TestClass<int *, char *> obj1;
TestClass<const int *, char *> obj2;
return 0;
}
对于模板、模板的特化和模板的偏特化都存在的情况下,编译器在编译阶段进行匹配时,是如何抉择的呢?从哲学的角度来说,应该先照顾最特殊的(全特化版本),然后才是次特殊的(偏特化版本),最后才是最普通的(模板版本)。编译器进行抉择也是尊从的这个道理。从上面的例子中,我们也可以看的出来,这就就不再举例说明。
模板多态的缺点
然而,当考虑到构建一个工程,并且要让多个工程人员参与开发的时候,编译器多态可能未必是个好的选择。1 模板开发的诸多禁忌,比如模板特化之于头文件定义,偏特化和全特化顺序陷阱等等;2 模板开发的编译报错对工程人员的基本功要求更高;3 基于接口的多态,更像一个简单的规约,易于理解和遵守。 多少人都是std或者boost模板的重度使用者,多少人离了STL寸步难行。但有多少人能捻熟地给出一个完全标准的STL的实现?或有人是碍于算法的短板,但相信更多人是碍于语法的短板。
补充 在c++的多态中,通过父类的指针获取实际子类的类型
需要的是运行期的类型,C++ 里挺有争议(跟异常类似)的功能——RTTI。(还是要强调一句,你应该考虑是否用虚函数可以达到你需要的功能。很多项目,如 Google 的,会禁用 RTTI。)
可以用 dynamic_cast 来转换成你需要的指针类型,如果类型不对,会得到空指针。你也可以用 typeid 直接来获取对象的实际类型。
#include <iostream>
#include <typeinfo>
#include <boost/core/demangle.hpp>
using namespace std;
using boost::core::demangle;
class shape {//父类 shape
public:
virtual ~shape() {}
};
class circle : public shape {//子类circle
};
int main()
{
shape* ptr = new circle();//父类指针指向子类对象 实现多态
auto& type = typeid(*ptr);//通过typeid 使用父类指针 获得子类的类型名
cout << type.name() << endl;
cout << demangle(type.name()) << endl;
cout << boolalpha;
cout << (type == typeid(shape) ? "is shape\n" : "");
cout << (type == typeid(circle) ? "is circle\n" : "");
delete ptr;
}
在 GCC 下的输出:
6circle
circle
is circle