C++中的类型擦除(type erasure in c++)

2022-11-25,,

作者:唐风
出处:
http://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++ 的类型检查系统中工作,维持着类型安全上的优点。