作者:唐风
原载:www.cnblogs.com/liyiwen
关于类型擦除,在网上搜出来的中文资料比较少,而且一提到类型擦除,检索结果里就跑出很多 Java 和 C# 相关的文章来(它们实现“泛型”的方式)。所以,这一篇我打算写得稍微详细一点。 注意,这是一篇读书笔记(《C++ template metaprogramming》第9.7小节和《C++ テンプレートテクニック》第七章),里面的例子都来自原书。
在 C++ 中,编译器在编译期进行的静态类型检查是比较严格的,但有时候我们却希望能“避过”这样的类型检查,以实现更灵活的功能,同时又尽量地保持类型安全。听起来很矛盾,而且貌似很难办到。但其实 C++ 的库里已经有很多这样的应用了。比如,著名的 boost::function 和 boost::any 。当我们定义一个 function<void(int)> fun 对象,则 fun 即可以存储函数指针,又可以存储函数对象,注意,这两者是不同“类型”的,而且函数对象可以是无限种类型的,但这些不同类型的“东西”都可以存在同一类型的对象 fun 中,对 fun 来说,它关心的只是存储的“对象”是不是“可以按某种形式(如void(int))来调用”,而不关心这个“对象”是什么样的类型。有了 function 这样的库,在使用回调和保存可调用“对象”的时候,我们就可以写出更简单且更好用的代码来。再举一个例子,boost::any 库。any 可以存储任何类型的“对象”,比如 int ,或是你自己定义的类 MyCla 的对象。这样我们就可以使一个容器(比如 vector<boost::any> )来存储不同类型的对象了。
这些库所表现出来的行为,就是这篇文章中要提到的类型擦除,类型擦除可以达到下面两个目的:
- 用类型 S 的接口代表一系列类型 T 的的共性。
- 如果 s 是 S 类型的变量,那么,任何 T 类型的的对象都可以赋值给s。
好了,下面我们具体地看看类型擦除是怎么回事,在这个过程中,我们先以 any 这个类为依托来解释(因为它比较“简单”,要解释的额外的东西比较少)。
any 这个类需要完成的主要任务是:1. 存储任何类型的变量 2. 可以相互拷贝 3. 可以查询所存变量的类型信息 4. 可以转化回原来的类型(any_cast<>)
对于其中,只要说明1和2 ,就能把类型擦除的做法展示出来了,所以,我们这里只实现一个简单的,有1、2、3功能的any类(3是为了验证)。
首先,写个最简单的“架子”出来:
class my_any { ?? content_obj; public: template <typename T> my_any(T const& a_right); };
这里,由于 my_any 的拷贝构造函数使用的是模板函数,因此,我们可以任何类型的对象来初始化,并把该对象的复本保存在 content_obj 这个数据成员中。那么,问题是,content_obj 用什么类型好呢?
首先我们会想到,给 class 加个模板参数 T ,然后……,不用然后了,这样的话,使用者需要写这样的代码:
my_any<someType> x = y;
不同的 y 会创造出不同类型的 x 对象,完全不符合我们要将不同类型对象赋给同一类型对象的初衷。接着,我们会想到用 void *(C 式的泛型手法啊……),但这样的话,我们就会完全地丢失原对象的信息,使得后面一些操作(拷贝、还原等)变得很困难,那么,再配合着加入一些变量用于保存原对象信息?你是说用类似“反射”的能力?好吧,我只好说,我以为 C++ 不存在原生的反射能力,以我浅薄的认识,我只知道像 MFC 式的侵入式手法……,嗯,此路不通。
这个困境的原因在于,在C++ 的类中,除了类模板参数之外,无法在不同的成员(函数、数据成员)之间共享类型信息。在这个例子中,content_obj 无法得知构造函数中的 T 是什么类型。所以类型无法确定。
为了妥善保存原对象复本,我们定义两个辅助类,先上代码(来自 boost::any 的原码):
class placeholder { public: // structors virtual ~placeholder() { } public: // queries virtual const std::type_info & type() const = 0; virtual placeholder * clone() const = 0; }; template<typename ValueType> class holder : public placeholder { public: // structors holder(const ValueType & value): held(value) { } public: // queries virtual const std::type_info & type() const { return typeid(ValueType); } virtual placeholder * clone() const { return new holder(held); } public: // representation ValueType held; };
首先,定义了一个基类 placeholder ,它是一个非模板的抽象类,这个抽象类的两个接口是用来抽取对保存在 my_any 中的各种类型对象的共性的,也就是,我们需要对被保存在 my_any 中的数据进行拷贝和类型查询。
然后用一个模板类 holder 类继承 placeholder 类,这个(类)派生类实现了基类的虚函数,并保存了相关的数据。注意,派生类的数据成员的类型是 ValueType,也就是完整的原对象类型,由于它是个模板类,各个类成员之间可以共享类模板参数的信息,所以,可以方便地用原数据类型来进行各种操作。
有了这两个辅助类,我们就可以这样写 my_any 了:
class My_any { placeholder * content_obj; public: template <typename T> My_any(T const& a_right):content_obj(new T(a_right)) {} template <typename T> My_any & operator = (T const& a_right) { delete content_obj; content_obj = new T(a_right); return *this; } My_any(My_any const& a_right) : content_obj(a_right.content_obj ? a_right.content_obj->clone() : 0) { } std::type_info& type() const { return content_obj ? content_obj->type() : typeid(void); } };
现在 my_any 类的 content_obj 的类型定义成 placeholder * ,在构造函数(和赋值运算符)中,我们使用 holder 类来生成真实的“备份”,由于 holder 是模板类,它可以根据赋值的对象相应地保存要我们需要的信息。这样,我们就完成了在赋值的时候的“类型擦除”啦。在 my_any 的 public 接口( type() )中,利用 placeholder 的虚函数,我们就可以进行子类提供的那些操作,而子类,已经完整地保存着我们需要的原对象的信息。
接着我们看下 boost::function 中的 Type Erasure。相比起 boost::any 来,function 库要复杂得多,因为这里只是想讲 boost::function 中的“类型擦除”,而不是 boost::function 源码剖析,所以,我们仍然本着简化简化再简化的目的,只挑着讨论一些“必要”的成分。
我们假设 function 不接受任何参数。为了更好的说明,我先帖代码,再一步一步解释,注意,下面是一片白花花的代码,几没有注释,千万别开骂,请跳过这段代码,后面会有分段的解释:
#include <iostream> #include <boost/type_traits/is_pointer.hpp> #include <boost/mpl/if.hpp> using namespace std; union any_callable { void (*fun_prt) (); // 函数指针 void * fun_obj; // 函数对象 }; template<typename Func, typename R> struct fun_prt_manager { static R invoke(any_callable a_fp) { return reinterpret_cast<Func>(a_fp.fun_prt)(); } static void destroy(any_callable a_fp) {} }; template<typename Func, typename R> struct fun_obj_manager { static R invoke(any_callable a_fo) { return (*reinterpret_cast<Func*>(a_fo.fun_obj))(); } static void destroy(any_callable a_fo) { delete reinterpret_cast<Func*>(a_fo.fun_obj); } }; struct funtion_ptr_tag {}; struct funtion_obj_tag {}; template <typename Func> struct get_function_tag { typedef typename boost::mpl::if_< boost::is_pointer<Func>, // 在 VC10 中标准库已经有它啦 funtion_ptr_tag, funtion_obj_tag >::type FunType; }; template <typename Signature> class My_function; template <typename R> class My_function<R()> { R (*invoke)(any_callable); void (*destory)(any_callable); any_callable fun; public: ~My_function() { clear(); } template <typename Func> My_function& operator = (Func a_fun) { typedef typename get_function_tag<Func>::FunType fun_tag; assign(a_fun, fun_tag()); return *this; } R operator () () const { return invoke(fun); } template <typename T> void assign (T a_funPtr, funtion_ptr_tag) { clear(); invoke = &fun_prt_manager<T, R>::invoke; destory = &fun_prt_manager<T, R>::destroy; fun.fun_prt = reinterpret_cast<void(*)()>(a_funPtr); } template <typename T> void assign (T a_funObj, funtion_obj_tag) { clear(); invoke = &fun_obj_manager<T, R>::invoke; destory = &fun_obj_manager<T, R>::destroy; fun.fun_obj = reinterpret_cast<void*>(new T(a_funObj)); } private: void clear() { if (!destory) { destory(fun); destory = 0; } } }; int TestFun() { return 0; } class TestFunObj { public: int operator() () const { return 1; } }; int main(int argc, char* argv[]) { My_function<int ()> fun; fun = &TestFun; cout<<fun()<<endl; fun = TestFunObj(); cout<<fun()<<endl; }
首先需要考虑的是,数据成员放什么?因为我们需要存储函数指针,也需要存储函数对象,所以,这里定义一个联合体:
union any_callable { void (*fun_prt) (); // 函数指针 void * fun_obj; // 函数对象 };
用来存放相应的“调用子”。另外两个数据成员(函数指针)是为了使用上的方便,用于存储分别针对函数指针和函数对象的相应的“操作方法”。对于函数指针和函数对象这两者,转型(cast)的动作都是不一样的,所以,我们定义了两个辅助类 fun_prt_manager 和 fun_obj_manager,它们分别定义了针对函数指针和函数对象进行类型转换,然后再引发相应的“调用”和“销毁”的动作。
接下来是类的两个 assign 函数,它们针对函数针指和函数对象,分别用不同的方法来初始化类的数据成员,你看:
invoke = &fun_prt_manager<T, R>::invoke; destory = &fun_prt_manager<T, R>::destroy; fun.fun_prt = reinterpret_cast<void(*)()>(a_funPtr);
当 My_function 的对象是用函数指针赋值时,invoke 被 fun_prt_manager 的 static 来初始化,这样,在“调用”时就把数据成员转成函数指针。同理,可以知道函数对象时相应的做法(这里就不啰嗦了)。
但如何确定在进行赋值时,哪一个 assign 被调用呢?我想,熟悉 STL 的你,看到 funtion_ptr_tag 和 funtion_obj_tag 时就笑了,是的,这里的 get_function_tag 用了 type_traise 的技法,并且,配合了 boost::mpl 提供的静态 if_ 简化了代码。这样,我们就完成了赋值运算符的编写:
template <typename Func> My_function& operator = (Func a_fun) { typedef typename get_function_tag<Func>::FunType fun_tag; assign(a_fun, fun_tag()); return *this; }
有了这个函数,针对函数指针和函数对象,My_function 的数据成员都可以正确的初始化了。
如我们所见,在 My_function 中,使用了很多技巧和辅助类,以使得 My_funtion 可以获取在内部保存下函数指针或是函数对象,并在需要的时候,调用它们。函数指针或是函数对象,一旦赋值给 My_funtion,在外部看来,便失去了原来的“类型”信息,而只剩下一个共性——可以调用(callable)
这两个例子已经向你大概展示了 C++ 的“类型擦除”,最后,再补充一下我的理解:C++中所说的“类型擦除”不是有“标准实现”的一种“技术”(像 CRTP 或是 Trais 技术那样有明显的实现“规律”),而更像是从使用者角度而言的一种“行为模式”。比如对于一个 boost::function 对象来说,你可以用函数指针和函数对象来对它赋值,从使用者的角度看起来,就好像在赋值的过程中,funtion pointer 和 functor 自身的类型信息被抹去了一样,它们都被“剥离成”成了boost::function 对象的类型,只保留了“可以调用”这么一个共性,而 boost::any ,则只保留各种类型的“type查询”和“复制”能力这两个“共性”,其它类型信息一概抹掉。这种“类型擦除”并不是真正的语言层面擦除的,正如我们已经看到的,这一切仍然是在 C++ 的类型检查系统中工作,维持着类型安全上的优点。