C++泛型与多态(1):基础篇

转载自:https://www.jianshu.com/p/38f17600f19a

作者:袁英杰

C++的泛型编程是一种非常强大的武器。但它看上去复杂的语法,以及背后不明的原理,一直让很多程序员望而生畏。很多即便已经使用了C++很久的程序员也仅仅敢触碰其非常表象的部分。还有一些大胆而莽撞的程序员则对其进行滥用,让本来可以简单解决的问题,变成秀技场。因而,泛型技术一直得到一个不公正的名声:奇技淫巧。

但是,泛型编程对于很多问题的高效解决是不可或缺的。它是C++最重要的元编程工具,也是很多框架制作的利器,也是组合式设计的主要手段之一。不掌握它,C++的强大威力将大打折扣。

我们需要深入挖掘其背后的原理及本质,这样才能理解它,掌握它,在合适的场合使用它,避免滥用和无用。

C++的泛型编程,其本质其实非常简单:为了做基于类型常量编译时多态。(关于多态的目的和价值,请参阅《多态,FP与OO》)

C++编译时多态由如下四种类型构成:

  1. 模版
  2. 函数(操作符)重载
  3. 模板特化
  4. Duck Typing

我们下面一一介绍。

1. 模版:参数化多态

先看一段简单的haskell代码:

max :: (Ord a) => a -> a -> a
max x y = if x > y then x else y

在这个实现里,a是类型变量,(Ord a)表示对a约束,意思是:对于所有属于typeclass Ord的类型a,都可以实例化这个函数。

这种用法被称作参数化多态。很显然,参数化多态是为了避免基于类型的重复代码

而回到C++,基于模版的实现则为:

template <typename T>
T max(T a, T b)
{
   return a > b ? a : b;
}

参数化多态,是C++提供泛型的最初目的。STL的各种容器和算法,基本上都是基于这类目的设计的。这也是最容易理解,被使用最为广泛的C++编译时多态技术。

Concept

上述haskell代码中的约束Ord a,到C++14为止尚未支持,但已经确定要在C++17中支持(被称作Concept)。

高阶类型

上述haskell例子中的多态类型属于rank 1 type,但rank 1 type无法支持这类的代码:

g f = (f True, f 'a')

其中,参数f作为一个函数,在两次调用时传入的参数类型不同:分别为BoolChar。这属于Rank 2 Type的范畴。而为了支持Rank 2 TypeGHC提供了一个扩展`RankNTypes’,并且要求程序员给出类型注解:

{-# LANGUAGE RankNTypes #-}

g :: (forall a. a -> a) -> (Bool, Char)
g f = (f True, f 'a')

而在C++中,对应的实现技术为Template Template Parameter。比如:

template <typename T, typename G,
template <typename E> class Container >
struct Foo
{ 
  Container<T> tContainer; 
  Container<G> gContainer;
};

这就是参数化多态的全部:非常简单,因而被很多语言采纳和引入,也被程序员广泛的使用。

2. 函数重载:Ad-hoc多态

学习任何一门语言的第一个例子,往往都是Hello World:

std::cout << "Hello, 2016!" << std::endl;

如果我们让程序稍微复杂一些,让这段代码可以对在任何一年都可以复用,那么我们可以提供一个类似于下面的函数:

void helloNewYear(unsigned int year) 
{ 
  std::cout << "Hello, " << year << "!" << std::endl;
}

在这个实现中,对于字符串整数这两种不同类型的数据,std::cout可以用一致的方式来操作。

按照多态四要素std::cout即是客户同一外表是操作符<<;而不同形态则是针对不同类型的不同实现:

std::ostream& operator<<(std::ostream& os, const std::string& str);
std::ostream& operator<<(std::ostream& os, const int value);
// ... 

而编译器会根据类型进行匹配(这是一种简化版本的模式匹配),从而在不同形态间进行选择

因而函数重载是一种多态,而这样的多态被称作ad-hoc多态。可参看Ad-hoc多态是什么?

例子:双重派发

双重派发Double Dispatch),是访问者模式(Visitor Pattern)依托的的实现技术。

假设,一颗语法树,上面有多种类型的节点。比如:StatementExpression

而一个访问者则必须提供针对各种类型节点的访问函数,比如:

struct Visitor 
{ 
   virtual void visit(Statement&) = 0; 
   virtual void visit(Expression&) = 0; 
   virtual ~Visitor() {} 
}; 

所有的节点都需要实现accept方法,所以它们都需要实现下面的接口:

struct Element 
{ 
  virtual void accept(Visitor&) = 0; 
  virtual ~Element() {} 
}; 

而每个具体的节点在实现此接口时,分别调用Visitor中针对自己类型的成员函数:

struct Statement: Element 
{ 
  void accept(Visitor& visitor) 
  { 
    visitor.visit(*this); 
  } 
    
  // ... 
};
 
struct Expression: Element 
{ 
  void accept(Visitor& visitor) 
  { 
    visitor.visit(*this); 
  } 
  // ... 
};

所谓双重派发,指的正是两个多态结构:

  1. 访问算法通过接口accept,在运行时将visitor派发给相应的 Element;
  2. 具体的Element再从visitor的多个访问函数中,选择归属于自己的那一个,通过它,将自己派发给visitor

其中,前者使用的是运行时多态,后者使用的则是函数重载。有了函数重载,就完全没必要让每个具体的Element重复的编写完全相同的accept实现。所以,我们可以将上述代码重构为:

template <typename T>
struct AutoDispatchElement : Element 
{ 
  void accept(Visitor& visitor)
  { 
     visitor.visit((T&)*this); 
  } 
};
 
struct Statement 
   : AutoDispatchElement<Statement>
{ 
  // ... 
};
 
struct Expression
   : AutoDispatchElement<Expression> 
{ 
  // ... 
};

并非所有重载都是为了多态

需要强调的是:尽管函数(操作符)重载可以用来实现编译时多态,但并非所有的重载都是为了多态。大多数情况下,操作符的重载都是为了提高表达力,比如:

// 让复数可以进行 a + b 形式的操作
Complex operator+(const Complex& lhs, const Complex& rhs); 

// 让向量可以进行 vector[5] 形式的操作
template <typename T>
T Vector<T>::operator[](unsigned int index); 

至于那些参数个数不同的重载函数,与多态更是没有什么关系,比如:

void f(int a) 
void f(short a, double b); 

这种样式的重载,往往是因为开发人员懒得为之取一个更准确的名字,从而滥用了语言提供的重载机制,其结果反而伤害了表达力。对于这样的重载,要尽量避免。

小结

这一部分我们首先介绍了C++范型最为简单的两种多态:以基于类型的重用为目的参数化多态和以函数重载为手段的ad-hoc多态

在后续的文章里,会继续介绍更为复杂的C++编译时多态技术

相关链接

深入理解C++泛型(1):基础篇

深入理解C++泛型(2):模板特化

深入理解C++泛型(3):类模板特化

深入理解C++泛型(4):Duck Typing

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值