一、鸭子类型
鸭子类型不是从c++中出现的,duck typing这种称呼在Python中比较多见。那么什么是鸭子类型呢?它是动态类型的一种风格,只要是对象的特征(其方法和属性集)和某个类型一致,就认为它是某种类型。它一般在动态编译的语言中,如Python、Js等等中比较多见。而在静态语言如c#,c++等则需要一些特定的方式来支持实现。
鸭子类型强调是的从对象到类型,也就是说从具体到抽象的映射,而不是常见的由抽象到具体的映射。可以这样理解,通过类或者接口的继承实现的对象,一定是其实现的。反过来,一个对象(运行时),不知道它是怎么实现的,但是发现它和某个类或者接口很象(如上面所说方法和属性集),那么也认定是它实现的。
这有点过分啊,其实是等于必要和充要条件在某种情况下强行认定一致了。
先看一个python中的实现:
class Duck:
def shout(self):
print("Duck shot")
def fly(self):
print("Duck fly")
class Goose:
def shout(self):
print("Goose shout")
def fly(self):
print("Goose fly")
def test(bird):
bird.shout()
bird.fly()
test(Duck())
test(Goose())
鸭子类型的优势在于:它没有使用继承却实现了多态。这个在前面的元编程中包括CRTP中都对这种技巧介绍过,不通过继承实现多态的好处,早就清楚了。
回过头来,其实在c++中是没有鸭子类型一说的。但是在c++的模板元编程中,可以实现类似鸭子类型的实现方式,这也可以归到鸭子类型中。
二、模板中的实现
先看一个最简单的例子:
template<typename T>
bool max(const T& t1, const T& t2)
{
return t1 > t2;
}
这是一个最简单的模板函数,如果单纯就这个函数来说,没有什么不合理的地方。但实际上,这个函数中隐含了一个条件,即T这个类型必须可以进行大小的比较。而T本身又是一个模板类,这就出现问题了,如果传入自定义的一个类而这个类没有实现operator >,那么程序会有什么问题呢?如果用这个来说明鸭子类型(方法和属性集)的控制还不太清晰的话,可以看一看前面的一个例子:
template<typename T>
auto len (T const&& t) -> decltype((void)(t.size()) , T::size_type)
{
return t.size();
}
这个函数的形式通过decltype来控制t中是否拥有函数size()。这是不是就是鸭子类型中的方法集合控制。推而广之,就可以通过其来判断实现类似于鸭子类型的数据结构。但是需要注意的是c++元编程本身是并不支持鸭子类型的,这个一定要注意。如果在实际应用中需要使用,则需要通过对数据结构的封装(特别是对非类型参数和基本数据类型),来间接实现鸭子类型。
在c++20以后,可以通过概念的方式来对上述方法进行优化
template<typename T>
concept func_ask = requires(T t)
{
t.must();
};
//......省略
template<func_ask T>
void must(const T& t)
{
t.must();
}
//省略
完整的代码在前面的“concepts的几个应用”及“C++20中的concepts”中相关段落。
鸭子类型其实在c++这类强类型语言中,并没有多大的优势,它使得本来就比较难于维护的代码更是雪上加霜。但不代表鸭子类型一无是处,用到了应该用的地方,就好。就和用人一样,要用人之长而非人之短。
三、总结
其实这一章,确实对元编程来说没有多大的用处。之所以仍然写这一篇目的很简单,把一些语言层面的东西和具体的技术实现形成一个横向的对比。前面提到过,任何语言的发展,不是凭空想象出来的,一定是基于某种思想在实践中不断的反复锤炼而出的。它既上承上又是启下,它解决一些问题,同样可能引入了一些新的问题,但总体上一定是在某个方面上有了较其它语言的一个明显的进步,这才能使得这门语言能在软件世界里应用。
如果有兴趣,大家可以回头望一下从汇编到C再到c++语言。然后就是暴发式的Java、c#、JS、Ts、Go、Python、Rust等等。明白了这些,在学习一门新语言时,就会明白,这门语言并不是完眼的新技术。如果掌握的语言越多,越会发现,这里面依稀有很多熟悉的身影。如果要想举一个例子的话,最明显的就是Lambda表达式,几乎现在主流的语言都支持了。明白了吧。