zoukankan      html  css  js  c++  java
  • C++中的类型擦除(type erasure in c++)

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

  • 相关阅读:
    27. Remove Element
    26. Remove Duplicates from Sorted Array
    643. Maximum Average Subarray I
    674. Longest Continuous Increasing Subsequence
    1. Two Sum
    217. Contains Duplicate
    448. Find All Numbers Disappeared in an Array
    566. Reshape the Matrix
    628. Maximum Product of Three Numbers
    UVa 1349 Optimal Bus Route Design (最佳完美匹配)
  • 原文地址:https://www.cnblogs.com/muxue/p/1621451.html
Copyright © 2011-2022 走看看