C++的多态

虽然没有学过Java,但是很喜欢Java的干净、统一,没有特别多的意外情况。因为Java里“一切皆是对象”。所有的类型都继承自Object类型,所以能写出一个适用于一切类型的函数。这是一个良好的实践,符合设计模式的基本特征。

所以当我们看许多C++框架的时候,这个框架都会有一个Object类,框架里所有的类都继承自这个类——这也算是自成一个小系统了。

但是,在写C++程序时,我时常会遇到一个问题——并不是一切皆是Object。当我们费尽心思写出一个能够处理所有类型的函数时,譬如:

template<class T> void f(T t)
{
第一步,调用t的构造函数
第二步,XXXX
}

我们发现,写函数的时候,都会对参数有某些假设,譬如,所有的参数都可以加减乘除,所有的参数都可以调用构造函数。没错,如果所有的类型都由我们自己来定义,确实没问题。但是——C++有自己的原生类型,C++的POD(Plain Old Data),也就是标量类型或传统类型。

当我们写出一个函数时,世界一片和谐一片大好。但是,突然发现,有那么一点不和谐。有一点东西就是纳入不到我们的框架之内,这时候我们需要单独地、特别地来处理这些不和谐的因素。

上述所说的“一切皆对象”是所有面向对象编程语言都提供的东西。通过继承来实现运行时多态。但是C++提供了函数重载和模板偏特化这两种工具来实现编译时多态。相比于运行时多态,编译器多态比较麻烦,但是随度更快。

1. 函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称为重载函数(overloaded)。
而掌握函数重载的关键是掌握函数匹配的原则。

1.1 函数匹配

  1. 第一步是选定本次调用对应的重载函数集,也就是候选函数。候选函数有两个要求:一是与被调用函数同名;二是其声明在调用点可见。
  2. 根据实参,从候选函数中选择可行函数。可行函数有两个特征:一是形参数量与本次调用提供的实参数量相等;二是每个实参的类型与对应形参类型相同,或者是能转换成形参的类型
  3. 寻找最佳匹配,其基本思想是:实参类型与形参类型越接近,匹配得越好。而这就是函数重载发挥作用的地方。
  4. 如果找不到唯一的最佳匹配,则产生二义性编译错误。

而函数重载的难点在于,实参类型可以转换成形参类型时,如何确定最佳匹配。

1.2 C++中的类型转换

  • 由const引起的类型转换。
    • 对于顶层const来说,譬如const int,与int类型编译器同等对待,两者等价;
    • 对于底层const来说,其实主要指的是指向常量的指针对常量的引用,允许指向非常量指针/对非常量的引用转换为指向常量的指针/对常量的引用。
  • 由于继承引起的类型转换。
  • 算术转换,也就是算术类型之间的转换。
    • 整型提升。把小整数类型转换为较大的整数类型。
    • 其他算术类型转换。
  • 数组与指针的等价性
  • C++规定的指针转换方式:nullptr可以转为任意指针类型;而任意指针可以转为const void*,任意指向非常量的指针可以转为void*。
  • 类类型转换。这是由重载类型转换运算符来完成的。编译器每次只能执行一次。可以置于上述标准的类型转换之前和之后。

1.3 函数匹配的最佳匹配等级

  1. 精确匹配:分情况:
    • 实参类型与形参类型相同;
    • 实参从数组类型或函数类型转换成指针类型
    • 实参添加/删除顶层const。
  2. 通过const转换实现的匹配;
  3. 通过类型提升实现的匹配(有int型形参,有short形参,则char直接提升为int)
  4. 通过算术类型转换实现的匹配,所有转换等级都一样;
  5. 通过类类型转换实现的匹配。

1.4 补充说明

当函数重载与类这两个概念发生联系的时候,我们需要有几点问题需要注意。

void f(int);
class A
{
void g(){f(3);}
void f(double);
}

上述情况下,成员函数g中调用的f是哪一个?

名字查找(name lookup):

  • 在名字所在块中寻找其声明语句,只考虑在名字的使用之前出现的声明;
  • 如果没找到,继续查找外层作用域;
  • 最终没找到,程序报错。

这就牵扯到了类中名字查找的一些特点:

类作用域的特点是:编译器分两步来处理:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

成员函数中使用的名字解析方式如下:

  • 在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。
  • 如果成员函数内没有找到,则在类内继续查找;
  • 如果类内也没有找到,在成员函数定义之前的作用域内继续查找。

第二个问题是由运算符重载引起的。

a sym b

可能调用的是:

a.operatorsym(b);//a 的成员函数
operatorsym(a, b);//普通函数

当在表达式中使用重载的运算符时,要考虑上面两种情况。

2. 模板

2.1 模板与函数重载

就像引入类会给函数重载带来一些新的规则;引入模板也会给函数重载带来一些新的规则:

  • 对于一个调用,其候选函数包括所有的模板参数推断成功的函数模板实例;
  • 如果同样好的函数中只有一个是非模板函数,则选择此函数;
  • 如果同样好的函数中没有非模板参数,而有很多函数模板,且其中一个模板比其他模板更特例化,则选择此模板。(没有他的话,本质上(const T&)可以用于任何类型);
  • 否则,程序报错

ps. [具体模板参数推断、类型转换等等细节,不在此讨论]

2.2 函数模板的特例化

通用模板:

template<typename T> int compare(const T&, const T&);

特例化示例:

template<>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}

一个特例化版本本质上是一个实例,而非函数名的一个重载版本。因此,特例化不影响函数匹配

2.3 类模板特例化(全特例化)

通用情况:

template<typename T>
class hash
{
}

特例化:

template<>
class hash<SaleData>
{
}

2.4 类模板部分特例化

不指定所有模板参数实参。只提供一部分模板参数或是参数的一部分而非全部特性。

通用版本:

tempalate<typename T> class remove_reference
{
typedef T type;
}

部分特例化版本:

template<typename T> class remove_reference<T&> //左值引用
{
typedef T type;
}

3. 应用

参考侯捷的《STL源码剖析》。

3.1 迭代器设计

现在我们需要这么一个功能,对于函数f,输入为迭代器,输出为迭代器的值。
这在C++11中很简单:

1 template<typename It>
2 auto f(It beg, It end) ->decltype(*beg)
3 {
4     return *beg;
5 }

但是在这之前,没有auto与decltype的时候,应该怎么实现呢?
我们假设所有的迭代器都有:

template<class T> class Iter
{
typedef T value_type;
}

那么就可以实现:

template<class It>
typename It::value_type f(It it)
{
return *it;
}

那么问题来了,我们的程序基于一个假设:所有的迭代器有value_type。
然而C++原生迭代器——指针——就不符合这个假定。
怎么办?需要使用特例化:

template<class T> T f(T* it) //迭代器是指针的情况
{
    return *it;
}

3.2 POD类型处理

现在需要一个功能,将迭代器[first, last)的内容填充为类型为T的val。
我们打算对于类型T调用构造函数;
然而,问题是int,float等等类型不需要构造函数,所以我们需要根据val来判断类型——而这正是C++所欠缺的。
那么我们可以针对这些POD类型进行特例化:

struct true_type{};
struct false_type{};

对于所有的类型有:

template<class Type>
struct type_trait
{
typedef false_type Is_POD;
}

针对char,int等POD类型有:

template<>
struct type_trait<char>
{
typedef true_type Is_POD;
}

那么,可以实现函数:

template<class It, class T>
void fill(It first, It last, const T& val)
{
fill_aux(first, last, val, value_type(first));
}
template<class It, class T, class T1>
void fill_aux(It first, It last, const T& val, T1*)
{
typedef typename type_trait<T1>::Is_POD is_POD;
fill_aux2(first, last, val, is_POD());
}
template<class It, class T>
void fill_aux2(It first, It last, const T& val, true_type)
{
for(auto it=first;it!=last;++it) *it =val;
}

template<class It, class T>
void fill_aux2(It first, It last, const T& val, false_type)
{
for(auto it=first;it!=last;++it) construct(&*it,val);
}

猜你喜欢

转载自blog.csdn.net/wangyanphp/article/details/79304541