深入了解C++中基于模板的类型擦除
作者:拉普拉斯妖kk
在C\C++中主要有三种类型擦除的方式:
基于void*的类型擦除,如C标准库的qsort函数。这中用法在C中是常见的。但因为是通过void*来操作数据,所以存在类型不安全的问题。
- 函数原型:void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *))
- 用途:对数组进行排序
- 类型擦除:base 是一个指向数组元素的指针,其类型为 void*。这使得 qsort 可以处理任何类型的数组。
面向对象的类型擦除,也就是C++中的继承,通过父类的引用或指针来调用子类的接口。这样解决了void*的类型不安全问题,但是继承也带来了代码复杂度提升,以及侵入式设计的问题(子类的实现比如知道父类和其继承体系)。
基于模板的类型擦除,技术上来说,是编写一个类,它提供模板的构造函数和非虚函数接口提供功能;隐藏了对象的具体类型,但保留其行为。典型的就是std::function。
下面是一个示例代码,实现了通用的任务,这些任务可以是任意的函数对象。
#include <iostream> #include <memory> // 抽象基类TaskBase作为公共接口不变;其子类TaskModel写成类模板的形式,其把一个任意类型F的函数对象function_作为数据成员。 struct TaskBase { virtual ~TaskBase() {} virtual void operator()() const = 0; }; template <typename F> struct TaskModel : public TaskBase { F functor_; template <typename U> // 构造函数是函数模板 TaskModel(U &&f) : functor_(std::forward<U>(f)) { } void operator()() const override { functor_(); } }; // 对TaskModel的封装 class MyTask { std::unique_ptr<TaskBase> ptr_; public: template <typename F> MyTask(F &&f) { using ModelType = TaskModel<F>; ptr_ = std::make_unique<ModelType>(std::forward<F>(f)); } void operator()() const { ptr_->operator()(); } }; /////////////测试代码///////////////// // 普通函数 void func1() { std::cout << "type erasure 1" << std::endl; } // 重载括号运算符的类 struct func2 { void operator()() const { std::cout << "type erasure 2" << std::endl; } }; int main() { // 普通函数 MyTask t1{&func1}; t1(); // 输出"type erasure 1" // 重载括号运算符的类 MyTask t2{func2{}}; t2(); // 输出"type erasure 2" // Lambda MyTask t3{ []() { std::cout << "type erasure 3" << std::endl; }}; t3(); // 输出"type erasure 3" return 0; }
总结下,要实现基于模板的类型擦除主要有三层的代码。
- 第一层是concept,TaskBase。考虑需要的功能后,以虚函数的形式提供对应的接口I。
- 第二层是model,TaskModel。这是一个类模板,用来存放用户提供的类T,T应当语法上满足接口I;重写concept的虚函数,在虚函数中调用T对应的函数。
- 第三层是wrapper,对应MyTask。存放一个concept指针p指向model对象m;拥有一个模板构造函数,以适应任意的用户提供类型;以非虚函数的形式提供接口I,通过p调用m。
从技术上来说,这种类型擦除的技巧可算是运行时多态的一种另类实现。它避免了一个类通过继承这种带来类间强耦合关系的方式,去满足某个运行时多态使用(polymorphic use)的需求。
测试代码表明,用户可以把任意的满足void()签名接口的函数对象作为任务,但不需要手动继承任何的代码或编写虚函数。实现任务类的运行时多态的代码被限制在库的范围内,不会以继承的方式侵入用户的代码或其他的库。
这里还有另一个示例。
首先,定义图形的概念接口ShapeConcept,接口类中定义了接口函数Draw。然后,通过模板ShapeModel具体实现了ShapeConcept的概念。最后,定义Shape类来封装ShapeModel。
这样,定义了Draw接口的正方形Square或者是通过特化实现了Draw的圆形Circle,都可以统一到Shape下,而不需要继承它。
#include <iostream> #include <memory> #include <utility> #include <vector> // 图形的概念接口 struct ShapeConcept { virtual void Draw() const = 0; }; // 图形概念的具体实现 template <typename T> struct ShapeModel : ShapeConcept { ShapeModel(T &&val) : shape_{std::forward<T>(val)} {} void Draw() const override { shape_.Draw(); // 这里假设具体图形有Draw()成员函数。如果没有,需要特化该模板。 } private: // 这里直接存储具体图形的值 T shape_; }; // 父类 class Shape { public: template <typename T> Shape(T &&val) : pimpl_{new ShapeModel<T>(std::forward<T>(val))} {} inline void Draw() const { pimpl_->Draw(); } private: std::unique_ptr<ShapeConcept> pimpl_; }; //---------------------正方形------------------------- class Square { public: explicit Square(float side) : side_(side) {} // 正方形的绘图函数是一个成员函数 void Draw() const { std::cout << "Draw square of side: " << side_ << std::endl; } private: float side_; }; //---------------------圆----------------------------- class Circle { public: explicit Circle(float radius) : radius_(radius) {} float GetRadius() const { return radius_; } private: float radius_; }; // 圆的绘图是一个全局函数 void DrawCircle(const Circle &circle) { std::cout << "Draw circle of radius: " << circle.GetRadius() << std::endl; } // 因为圆的绘图函数是一个全局函数,所以需要特化 template <> struct ShapeModel<Circle> : ShapeConcept { ShapeModel(Circle &&val) : shape_(std::move(val)) {} void Draw() const override { DrawCircle(shape_); } private: Circle shape_; }; int main() { std::vector<Shape> shapes; shapes.push_back(Circle(1.0)); shapes.push_back(Square(2.0)); for (const auto &shape : shapes) { shape.Draw(); } return 0; }
到此这篇关于深入了解C++中基于模板的类型擦除的文章就介绍到这了,更多相关C++类型擦除内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!