Type erasure

type erasure这个术语在c++中很少被提及,但是它所代表的技术却被非常广泛的应用。很多人听到这个词的第一印象可能是多态,通过继承与虚函数override来实现。但其实当我们在c++中聊到type erasure,他更多的是表示通过模板和继承来共同实现的。std::any 就是一个例子。不过这篇文章我们介绍一下所有和type erasure概念有关的技术。

基于void*

void*几乎可以说是任何类型的alias,所以当我们用这个的时候大部分就是将某个类型转换成void*然后传递。比如

void* bsearch (const void *key, 
              const void *base,
              size_t nmemb, 
              size_t size,
              int (*compar)(const void*, const void*));

为了让他能处理多种类型,我们把参数射程了void*. 这样类型就被我们擦除了,但问题是这样会造成类型安全不能保证,当你keycompar所接受的类型不一样时,就会在运行时出现Undefined Behavior错误,看如下例子:

int compareChar(const void*a , const void* b)
{
    return -strcmp ((const char *)a, (const char *)b);
}
bool findInVector(std::vector<int>& vec, int val)
{
    bsearch(&val, vec.data(), vec.size(), sizeof(int), compareChar);
}

基于Obeject oriented继承

比如我们有如下代码:

struct Circle : public Shape
{
public:
    void draw() override
    {
        //...impl
    }
};

void drawAShape(Shape* s);

通过继承自一个基类,然后在接口里传入基类的指针。 这个和void*比起来的好处是不管真正被调用的是什么类型,他一定有相应的函数可以被调用,所以不会出现UB,也保障了类型安全。但我感觉这算不上真正的类型擦除,因为他只是擦除了调用处的对象类型。他的问题在于要求每个实现类型必须继承自某一个基类,很多时候过多的继承关系会有耦合问题或者其他设计问题。

template

比如std::function,
std::function<bool(int, int)> predicate;,他对于type的要求是非侵入性的,即duck-typing-like(如果一个东西走路像鸭子,叫的像鸭子,那他就是鸭子)。从std::function的例子我们可以看到,它对类型的唯一要求就是含有bool operator()(int, int)

继承与temnplate的结合

std::any(c++17)就是一个很好的例子,它允许你通过any这个类来保存任何类型对象,而且不需要在构建时指定模板参数。

std::any a = 1;
std::cout << std::any_cast<int>(a) << std::endl; // output 1
a = 3.14;
std::cout << std::any_cast<double>(a) << std::endl; // output 3.1
std::cout << a.is<int>(); //output false

因为不能接受模板参数,我们需要在内部含有一个类模板来实现构造时的类型推导。但是如果仅仅有这个类模板,我们发现还是要从any外面传入模板参数才能写出构造函数,这样与我们的初衷不符。所以我们需要一个基类来作为any的成员变量,然后让一个类模板继承自这个基类来完成类型推导。基类会作为接口,其内部需要定义我们我们需要的方法的虚函数.由于我们会在any类内部创建一个以Base为基类的Derived对象,所以我们可以用unique_ptr来管理生命周期

struct any
{
    template <typename U>
    any(U&& value) : m_ptr(new Derived<typename std::decay<U>::type>(std::forward<U>(value))){}

    template <typename U>
    any& operator=(const any& rhs)
    {
        if(m_ptr == rhs.m_ptr)
            return *this;
        m_ptr = std::unique_ptr<Base>(new Derived<U>(rhs.m_ptr->m_value));
    }

    struct Base
    {
        virtual ~Base(){}
    };

    template<typename T>
    struct Derived : Base
    {
        template <typename U>
        explicit Derived(U&& value) : m_value(std::forward<U>(value)){}

        T m_value;
    };

std::unique_ptr<Base> m_ptr;
};

接下来我们实现isany_cast.我们可以利用dynamic_cast的特性去做检测,即如果不能被安全的cast成一个派生类,它会返回nullptr。当然我们也可以选择在any类里存储std::type_index信息,这样在性能上应该可以更好。

struct any
{
    // .....省略上面写过的代码

    template<class U>
    bool is() const
    {
        return dynamic_cast<Derived<U>*>(m_ptr.get()) != nullptr;
    }

    template <class U>
    U& any_cast()
    {
        auto d = dynamic_cast<Derived<U>*>(m_ptr.get());
        if(d == nullptr)
        {
            throw bad_cast();
        }
        return d->m_value;
    }
};

以上大概是std::any的一个简陋实现,Derived通过模板来存储了真实类型,而Base作为含有接口的抽象类在any被赋值时将类型进行擦除。这种技术在平时工作中的使用场景非常多,比如说你有如下需求:

  • 用同一种方式处理不同类型
  • 用同一种类型容器保存不同类型对象