对于 C++ 中的匿名函数,除了写 auto 外,还可以使用 std::function 作为类型接受匿名函数:
| 12
 3
 
 | std::function<void(int)> func = [](int x) {std::cerr << x << "\n";
 };
 
 | 
问题来了,刚开始的时候我以为 std::function<void(int)> 就是匿名函数的返回类型,在 github 上给别人发送 PR  时就发生了笑话。
实际上这两个类型并不相同,function 是一个类型擦除容器,而 lambda 匿名类型简单来说就是重载了 operator() 的类。由于 std::function 有转换构造函数,lambda 表达式得以调用这个转换构造函数,构造出这一个 std::function对象,所以这个赋值发生了隐式类型转换。
在一些代码中,我们可能无法保留原有的数据类型,上面的匿名函数就是典型的例子。这个时候需要用一种通用的类型去使用它们,需要去掉对象原有的数据类型,也就是类型擦除 (Type Erasure)。
常见的类型擦除
Void* 擦除
C 语言中的类型擦除技术为 void*,比较常见的场景是 memset,接受一个 void* 指针,可以将给定字节的内容置为 value。
| 1
 | void * memset ( void * ptr, int value, size_t num );
 | 
但是 void* 有很大的缺陷,在某些场景需要恢复数据类型才能使用。这就需要开发者在编程的时候时刻注意,运行到这里时,变量应该是什么类型,维护极其困难。
| 12
 3
 
 | void func(void *ctx) { auto sparse_ctx = (SparseData *)(ctx);
 }
 
 | 
继承擦除
类的继承也可以实现类型擦除的效果,用基类的指针指向子类:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | #include <iostream>
 class Shape {
 public:
 virtual void show() = 0;
 };
 
 class Circle : public Shape {
 public:
 void show() {
 std::cerr << "I am a circle\n";
 }
 };
 
 class Rect : public Shape {
 public:
 void show() {
 std::cerr << "I am a rect\n";
 }
 };
 
 void func(Shape *obj) {
 obj->show();
 }
 
 int main() {
 Circle c;
 Rect r;
 func(&c);
 func(&r);
 
 return 0;
 }
 
 | 
但是在很多情况下,这是很难做到的,因为它要求每个实现类型都继承自某个基类,甚至是毫不相关的类型。
模板擦除
在 C++ 中,可以通过模板参数实例化的形式,变相的实现了类型擦除:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | template<typename T>void Compare(T* data1, T* data2, std::size_t size) {
 for (std::size_t i = 0; i < size; i++) {
 unsigned char *p1 = (unsigned char *)data1 + i;
 unsigned char *p2 = (unsigned char *)data2 + i;
 if (*p1 != *p2) {
 std::cerr << " compare failed \n";
 return;
 }
 }
 std::cerr << " compare success \n";
 }
 
 | 
但是使用模板并没有完全的擦除类型,T 仍然是函数原型的一部分。这样我们其实一直都保留着元素的具体类型信息,好处:
- 完整的类型安全性,没有任何环节丢掉了类型信息
- 因此不需要动态绑定,所有环节都是静态的,没有运行时性能损失
但也有坏处:
- 模板类型会作为模板函数或模板类的原型的一部分,即 vector<int>和vector<double>是两个类型,没办法用一个类型来表示
- 每次用不同的参数类型来实例化模板时,都会新生成一份代码,导致编译出来的二进制文件很大
Type Erasure
在 C++ 中我们可以结合继承与模板,实现出类型擦除技术,将不同类型用同一种类型表示。假设我们此时有一个计数器类 ClassA,可以完成递增和递减的操作:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | class ClassA {public:
 int a;
 ClassA() {
 a = 0;
 }
 
 void Increase() {
 a += 1;
 }
 
 void Decrease() {
 a -= 1;
 }
 
 void GetValue() {
 std::cerr << "val = " << a << "\n";
 }
 };
 
 | 
我们期望在 main 函数中进行类型擦除,当接受的类型为 ClassA 时,可以进行递增和递减;在接受的类型时,如 int 型的常数,也可以进行同样的计算。如下所示:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | int main() {CountContain c1 = ClassA{};
 c1.Increase();
 c1.GetValue();
 
 CountContain c2 = 5;
 c2.Increase();
 c2.GetValue();
 }
 
 | 
为了这个目的,我们来实现 CountContain 这个类,在类中封装一个智能指针来保管创建的对象,也就是 main 函数中的 c1 和 c2,并调用对象的 Increase 等方法。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | class CountContain {public:
 std::unique_ptr<???> m_ptr;
 
 template<typename T>
 CountContain(T t) : m_ptr{new ???(std::forward<T>(t))} {};
 
 void Increase() {
 m_ptr->Increase();
 }
 void Decrease() {
 m_ptr->Decrease();
 }
 void GetValue() {
 m_ptr->GetValue();
 }
 };
 
 | 
此时需要填充上述代码的问号 ??? 部分。在 std::unique_ptr<???> m_ptr 中,没有模板参数 T。模板参数 T 只存于构造函数中,用于创造具体的对象。还记得之前讲过的类继承擦除吗?
对的,在这里指针的 ??? 部分应该使用基类,而构造函数的 ??? 应该使用继承基类的子类。子类通过模板来保留类型信息,而通过基类来实现统一的存储与调用。
那么实现一个这样的基类 CounterBase 用于调用:
| 12
 3
 4
 5
 6
 7
 8
 
 | class CounterBase {public:
 virtual ~CounterBase() {};
 virtual void Increase() = 0;
 virtual void Decrease() = 0;
 virtual void GetValue() = 0;
 int val;
 };
 
 | 
在实现对应的子类 CounterImpl 保留模板信息。注意,由子类调用实例化的 m_impl(也就是 ClassA )的 Increase 方法完成最终调用。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | template<typename T>class CounterImpl : public CounterBase {
 public:
 T m_impl;          // 移动构造,将参数的 value 移动到 m_impl
 CounterImpl(T value) : m_impl(std::move(value)) {}
 void Increase() override {
 m_impl.Increase();
 }
 void Decrease() override {
 m_impl.Decrease();
 }
 void GetValue() override {
 m_impl.GetValue();
 }
 };
 
 | 
对于没有 Increase、Decrease、Count 接口的类型,比如内置类型 int,我们还可以特化模板 CounterImpl 来满足要求:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | template<>class CounterImpl<int> : public CounterBase {
 public:
 int m_impl;
 CounterImpl(int value) : m_impl(value) {}
 void Increase() override {
 m_impl++;
 }
 void Decrease() override {
 m_impl--;
 }
 void GetValue() override {
 std::cerr << "val = " << m_impl << "\n";
 }
 };
 
 | 
对于 int 类型,通过模板特化支持了 Increase 等方法,各种行为和 ClassA 类保持一致,那么我们就可以认为 CounterImpl<int> 类型是一个像 ClassA 的鸭子类型。 
如果一个东西,走路像鸭子,叫声也像鸭子,那么它就是鸭子。换句话说,如果一个东西,满足我们对鸭子的所有要求,那么它就是鸭子。
最终代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 
 | #include <iostream>#include <memory>
 
 class ClassA{
 public:
 int a;
 ClassA() {
 a = 0;
 }
 
 void Increase() {
 a += 1;
 }
 
 void Decrease() {
 a -= 1;
 }
 
 void GetValue() {
 std::cerr << "val = " << a << "\n";
 }
 };
 
 class CounterBase {
 public:
 virtual ~CounterBase() {};
 virtual void Increase() = 0;
 virtual void Decrease() = 0;
 virtual void GetValue() = 0;
 int val;
 };
 
 template<typename T>
 class CounterImpl : public CounterBase {
 public:
 T m_impl;
 CounterImpl(T &&value) : m_impl(value) {}
 void Increase() override {
 m_impl.Increase();
 }
 void Decrease() override {
 m_impl.Decrease();
 }
 void GetValue() override {
 m_impl.GetValue();
 }
 };
 
 template<>
 class CounterImpl<int> : public CounterBase {
 public:
 int m_impl;
 CounterImpl(int value) : m_impl(value) {}
 void Increase() override {
 m_impl++;
 }
 void Decrease() override {
 m_impl--;
 }
 void GetValue() override {
 std::cerr << "val = " << m_impl << "\n";
 }
 };
 
 class CountContain {
 public:
 std::unique_ptr<CounterBase> m_ptr;
 
 template<typename T>
 CountContain(T t) : m_ptr{new CounterImpl<T>(std::forward<T>(t))} {};
 
 void Increase() {
 m_ptr->Increase();
 }
 void Decrease() {
 m_ptr->Decrease();
 }
 void GetValue() {
 m_ptr->GetValue();
 }
 };
 
 int main() {
 CountContain c1 = ClassA{};
 c1.Increase();
 c1.GetValue();
 
 CountContain c2 = 5;
 c2.Increase();
 c2.GetValue();
 }
 
 | 
总结
如果有下面两个需求,可能是需要 Type Erasure 的:
- 需要用同一种方式处理不同的类型
- 需要用同一种类型或容器保存不同类型的对象
参考
- C++: Type Erasure
- std::function实现