抄书自这里

什么是类型擦除

对于Python这种动态语言,是不存在类型擦除这个概念的,比如

class Foo(object):
    def print(self):
        print 'foo'
 
class Bar(object):
    def print(self):
        print 'bar'
 
def do_print(obj):
    print obj.print()
 
do_print(Foo()) // print 'foo'
do_print(Bar()) // print 'bar'
 

这样也是合法的。

但是显然对于C++,这样是不行的,比如

class Foo {
public:
    void print() { cout << "foo" << endl; }
};
 
class Bar {
public:
    void print() { cout << "bar" << endl; }
};
 
void do_print(??? obj) { // <--
    obj.print();
}
 
do_print(Foo());
do_print(Bar());

显然do_print()不知道该填什么类型。

怎么解决?

void*

void*是一种方法,在C中这是一种常用方法,什么是void*,以下来自LLM

  • void* 可以存放任意类型对象的地址,也就是说,任何类型的指针都可以直接赋值给 void*,无需强制类型转换。
  • 由于 void* 没有具体的类型信息,编译器无法知道它指向的数据类型和大小,因此不能直接对 void* 进行解引用或指针运算,只有在将其强制转换为具体类型的指针后才能操作。
  • 简而言之,void* 是一种可以指向任何类型数据的“无类型指针”,但使用时需要进行类型转换才能访问实际数据

void *虽然好,但是也有显然的缺点,一个是这样虽然擦除了类型信息,但是最终使用的时候还是需要知道void*对应的是什么类型,再转换回去;以及这样其实很不安全,如果类型匹配不上,是一种不安全的转换防范。

虚函数

如果用虚函数给这两个类实现一个基类,那么是可以解决的,如下

class IPrintable {
public:
    virtual void print() = 0;
};
 
class Foo : public IPrintable {
public:
    virtual void print() { cout << "foo" << endl; }
};
 
class Bar : public IPrintable {
public:
    virtual void print() { cout << "bar" << endl; }
};
 
void do_print(IPrintable* obj) {
    obj->print();
}
 
int main() {
    do_print(new Foo());
    do_print(new Bar());
    return 0;
};

这样的坏处是,虚函数需要通过虚表确定子类的调用,存在额外的开销。

模板

模板可以如下简单地解决

template <typename T>
void do_print(T obj) {
    obj.print();
}

这样的好处是类型擦除在编译期就解决了,性能上没有任何损失,也不需要调用的时候再去进行类型转换。但是有一个致命的弱点,比如我想把FooBar的两个对象放到一个容器std::vector里,这是无法像虚函数一样做到的,因为没有共同的接口。

std::function

std::function和以上解决的问题不太一样,是用于解决可调用(callable)对象的类型擦除,例如以下

  • 函数
  • lambda
  • 函数指针
  • 仿函数
  • std::bind
  • std::function

同样的,希望和上面一样,实现类型擦除,能够类似f(obj)这样调用一个可调用的对象。比如有着多个不同类型的回调函数情况下,自然希望能够以一种统一的方式调用

// http://www.cplusplus.com/reference/functional/function/function/
#include <iostream>     // std::cout
#include <functional>   // std::function, std::negate
 
// a function:
int half(int x) {return x/2;}
 
// a function object class:
struct third_t {
  int operator()(int x) {return x/3;}
};
 
// a class with data members:
struct MyValue {
  int value;
  int fifth() {return value/5;}
};
 
int main () {
  std::function<int(int)> fn1 = half;                    // function
  std::function<int(int)> fn2 = &half;                   // function pointer
  std::function<int(int)> fn3 = third_t();               // function object
  std::function<int(int)> fn4 = [](int x){return x/4;};  // lambda expression
  std::function<int(int)> fn5 = std::negate<int>();      // standard function object
 
  std::cout << "fn1(60): " << fn1(60) << '\n';
  std::cout << "fn2(60): " << fn2(60) << '\n';
  std::cout << "fn3(60): " << fn3(60) << '\n';
  std::cout << "fn4(60): " << fn4(60) << '\n';
  std::cout << "fn5(60): " << fn5(60) << '\n';
 
  // stuff with members:
  std::function<int(MyValue&)> value = &MyValue::value;  // pointer to data member
  std::function<int(MyValue&)> fifth = &MyValue::fifth;  // pointer to member function
 
  MyValue sixty {60};
 
  std::cout << "value(sixty): " << value(sixty) << '\n';
  std::cout << "fifth(sixty): " << fifth(sixty) << '\n';
 
  return 0;
}

这种强大的封装自然存在这缺陷,在可以封装所有可调用对象的情况下,会存在较大的性能损失。

这种封装类似虚函数,相比lambda、函数、函数指针,虚函数的性能损失可能在5倍,std::function则在6倍。

使用LLM生成了一个std::function的简单原理

class CallableBase {
public:
    virtual ~CallableBase() = default;
    virtual R invoke(Args... args) = 0;
};
 
template<typename T>
class CallableImpl : public CallableBase {
    T func;
public:
    CallableImpl(T f) : func(std::move(f)) {}
    R invoke(Args... args) override {
        return func(std::forward<Args>(args)...);
    }
};
 
class Function {
    std::unique_ptr<CallableBase> callable;
public:
    template<typename T>
    Function(T f) : callable(new CallableImpl<T>(std::move(f))) {}
    R operator()(Args... args) {
        return callable->invoke(std::forward<Args>(args)...);
    }
};

可以发现本质上也是通过对所有类型的可调用对象实现了一个虚基类,然后通过一个pointer指向该对象,在构造的时候初始化该pointer为类型擦除的对象,在调用std::function的时候通过std::invoke调用,通过std::forward对参数进行完美转发。实际的性能损耗主要在虚函数上,以及智能指针的内存不友好存在的一定损失。