天秤座的唐风

总会有一个人需要你的分享~!- 唐风 -

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  13 随笔 :: 0 文章 :: 69 评论 :: 0 Trackbacks

作者:唐风

原载: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++ 的类型检查系统中工作,维持着类型安全上的优点。

posted on 2009-12-10 23:05 唐风 阅读(2492) 评论(9)  编辑 收藏 引用 所属分类: 语言技术

评论

# re: C++中的类型擦除(type erasure in c++) 2009-12-11 09:00 Touchsoft
学习了……  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-11 09:14 崇文
做个标记,等看到这章的时候再来看,目前只看到第五章呢。  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-11 13:21 Vitacy.Tan
union any_callable {
void (*fun_prt) (); // 函数指针
void * fun_obj; // 函数对象
};
不用分开对待,直接void * fun就行了.函数可以直接当对象使用.
template <typename R,typename F>
class callable
{
public:
static R call(void * fun){
return (*static_cast<F *>(fun))();
}
static void destory(void *fun){
delete static_cast<F*>(fun);
}
};
template <typename R>
class function
{
public:
R operator () (){
return (*m_call)(m_fun);
}
template <typename T>
void operator=(T fun){
m_fun = static_cast<void *>(new T(fun));
m_call=callable<R,T>::call;
m_destory=callable<R,T>::destory;
}
template <typename T>
R call(){
(*static_cast<T *>(m_fun))();
}
~function(){
(*m_destory)(m_fun);
}
private:
void * m_fun;
R (*m_call)(void *);
void (*m_destory)(void *);
};
  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-12 10:50 唐风
@Vitacy.Tan
嗯,试过了你的代码,确实在我的例子的情况下,你的做法没有问题而且更简洁。呵,说真的,第一次看到 new 一个 function pointer 对象,有点诧异。后来想通了,function pointer 也不过就是一个 pointer 而已。
但 boost::function 中确实对两者进行了区分对待,而且考虑了更多的情况情况:
/**
       * A buffer used to store small function objects in
       * boost::function. It is a union containing function pointers,
       * object pointers, and a structure that resembles a bound
       * member function pointer.
       
*/

      union function_buffer
      
{
        
// For pointers to function objects
        mutable void* obj_ptr;

        
// For pointers to std::type_info objects
        struct type_t {
          
// (get_functor_type_tag, check_functor_type_tag).
          const BOOST_FUNCTION_STD_NS::type_info* type;

          
// Whether the type is const-qualified.
          bool const_qualified;
          
// Whether the type is volatile-qualified.
          bool volatile_qualified;
        }
 type;

        
// For function pointers of all kinds
        mutable void (*func_ptr)();

        
// For bound member pointers
        struct bound_memfunc_ptr_t {
          
void (X::*memfunc_ptr)(int);
          
void* obj_ptr;
        }
 bound_memfunc_ptr;

        
// For references to function objects. We explicitly keep
        
// track of the cv-qualifiers on the object referenced.
        struct obj_ref_t {
          mutable 
void* obj_ptr;
          
bool is_const_qualified;
          
bool is_volatile_qualified;
        }
 obj_ref;

        
// To relax aliasing constraints
        mutable char data;
      }
;

在下浅薄,不能完全明白其中的道理,还请路过高人指点。
 

 
  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-13 11:53 唐风
@Vitacy.Tan
在网上找了一下相关的资料,发现这个问题其实已经有了比较多的“讨论”,还有专门论述的文章:
http://www.safercode.com/blog/2008/11/25/generic-function-pointers-in-c-and-void.html

简而言之,就是:
不能 void* 和 void(*f)() 合并成一个,原因是对于 C 标准而言,这两个“东西”并不保证一致,函数指针的大小可能比 void* (也就是指向数据的指针大小要大),可能会包含更多的信息,如果在这之间进行转换的话,在某些平台会产生未定义的行为(在 Windows 平台下,这样做是没问题的,参考http://stackoverflow.com/questions/1867698/casting-a-void-pointer-data-to-a-function-pointer 中的回复)。因此,这么做是为了可移植性的考虑。

相关原文如下:
Why can’t we use void* for a Generic Function Pointer?
This is because a void* is a pointer to a generic “data” type. A void * is used to denote pointers to objects and in some systems, pointers to functions can be larger than pointers to objects. So, if you convert amongst them, you’ll lose information and hence, the situation would be undefined and implementation dependent. Most compilers won’t even warn you if you convert between them but some might error out, if you try to call such a void * to function pointer converted. But even they might fail to alert you, if you take care of typecasting the call perfectly (Enclose in parentheses before function call brackets). And then, one fine day, you’ll try to compile and run your program on one of the aforementioned systems, and then keep on wondering why your program segfaults.

Note: C++ does allow this “conditionally” which means that such a conversion is allowed but a compiler is not bound to implement this feature, which again makes its usage circumspect.

另外,我还是不太明白,function pointer 可能携带的其它信息是什么,呵呵,再查查喽。

谢谢 Vitacy.Tan 的回复,让我能更深入的了解这方面的知识。
嗯,有人能参与讨论就是好啊,希望大家多指点。



  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-13 12:05 唐风
@Vitacy.Tan
话又说回来,在你的做法中,并没有直接在 function pointer 和 void* 之间进行转型,而是为 function pointer 创建(new)了一个对象,赋给 void*,这样做我感觉是没有问题的,这时候 void* 指向的仍然是一个“对象”,呵呵。
但这么做可能在“效率”上不如直接备份 function pointer 。毕竟,创建时候的 new 和调用时的转型加最后的 delete,都比直接存 function pointer 负担大。但这样做获得了代码上的一致性,我倒是挺喜欢的,呵呵(如果确实没有“我所不知道的问题的话”)。
  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-14 11:16 Vitacy.Tan
function pointer 可能携带的其它信息是什么
参数个数 类型 还有像int a(int b=1);
在C++里参数个数 类型在函数名里(指针好像也没这个),默认参数可以重载解决
  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-17 10:45 U2U
很感谢你这篇文章,写得很好!  回复  更多评论
  

# re: C++中的类型擦除(type erasure in c++) 2009-12-18 07:38 欲三更
恩,上面那个回复里用“指针的指针”规避对象指针和函数指针的大小不一样这一点,挺好的。  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理