对于 C++
中的匿名函数,除了写 auto
外,还可以使用 std::function
作为类型接受匿名函数:
1 2 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*
有很大的缺陷,在某些场景需要恢复数据类型才能使用。这就需要开发者在编程的时候时刻注意,运行到这里时,变量应该是什么类型,维护极其困难。
1 2 3
| void func(void *ctx) { auto sparse_ctx = (SparseData *)(ctx); }
|
继承擦除
类的继承也可以实现类型擦除的效果,用基类的指针指向子类:
1 2 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++
中,可以通过模板参数实例化的形式,变相的实现了类型擦除:
1 2 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
,可以完成递增和递减的操作:
1 2 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
型的常数,也可以进行同样的计算。如下所示:
1 2 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
等方法。
1 2 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
用于调用:
1 2 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
方法完成最终调用。
1 2 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
来满足要求:
1 2 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
的鸭子类型。
如果一个东西,走路像鸭子,叫声也像鸭子,那么它就是鸭子。换句话说,如果一个东西,满足我们对鸭子的所有要求,那么它就是鸭子。
最终代码如下:
1 2 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实现