C++中的类型擦除技术
文章目录
- 一、C++类型擦除Type Erasure技术
- 1.虚函数
- 2.模板和函数对象
- 二、任务队列
- 1.基于特定类型的方式实现
- 2.基于任意类型的方式实现
- 参考:
一、C++类型擦除Type Erasure技术
C++中的类型擦除(Type Erasure)是一种技术,用于隐藏具体类型并以类型无关的方式处理对象。 它允许在运行时处理不同类型的对象,同时提供一致的接口和行为。
类型擦除常用于实现泛型编程和多态性,其中需要处理不同类型的对象,但又希望以一致的方式进行操作和处理。
两个常见的类型擦除技术:虚函数,模板和函数对象
1.虚函数
- 使用虚函数是一种简单的类型擦除技术,通过将函数声明为虚函数,可以在派生类中重写该函数以提供具体实现。
- 然后,可以使用基类指针或引用来处理不同派生类的对象,而无需关心具体的类型。 虚函数机制提供了动态派发的能力,使得在运行时选择正确的函数实现。
派生类是普通类
class Base {
public:virtual void foo() {// 基类默认实现}
};class Derived1 : public Base {
public:void foo() override {// 派生类1的实现}
};class Derived2 : public Base {
public:void foo() override {// 派生类2的实现}
};void process(Base& obj) {obj.foo(); // 调用适当的派生类实现
}int main() {Derived1 d1;Derived2 d2;process(d1); // 调用Derived1的foo()process(d2); // 调用Derived2的foo()return 0;
}
在上述示例中,
- Base类具有虚函数 foo(),并且它的派生类 Derived1 和 Derived2 分别提供了自己的实现。
- process() 函数接受 Base 类型的引用,可以在运行时根据实际的对象类型来调用适当的 foo() 实现。
派生类是模板类
#include <iostream>struct Base {virtual void foo() const = 0;
};template <typename T>
struct Derived : public Base {void foo() const override {std::cout << "Derived<" << typeid(T).name() << ">::foo()" << std::endl;}
};void process(const Base& obj) {obj.foo(); // 调用适当的派生类实现
}int main() {Derived<int> d1;Derived<double> d2;process(d1); // 调用Derived<int>::foo()process(d2); // 调用Derived<double>::foo()return 0;
}
在上述示例中,Base 是一个抽象基类,其派生类 Derived 是一个模板类。
- 模板参数 T 表示派生类的具体类型。通过在 Derived 类中使用 typeid 和 name(),可以在运行时获取具体类型的信息。
- process() 函数接受 Base 类型的常量引用,并调用适当的 foo() 实现。
2.模板和函数对象
另一种类型擦除的方法是使用模板和函数对象(Functor)。通过使用模板和函数对象,可以将类型信息推迟到运行时,并以一致的方式使用对象。
使用仿函数
- 这个例子展示了如何使用函数对象实现类型擦除,通过函数对象的模板化操作符 operator(),我们可以在运行时以一致的方式处理不同类型的对象。
#include <iostream>
#include <typeinfo>struct Base {virtual void foo() const = 0;
};struct Functor {template <typename T>void operator()(const T& obj) const {std::cout << "Functor: " << typeid(T).name() << std::endl;obj.foo();}
};template <typename T>
struct Derived : public Base {void foo() const override {std::cout << "Derived<" << typeid(T).name() << ">::foo()" << std::endl;}
};int main() {Derived<int> d1;Derived<double> d2;Functor f;f(d1); // 调用Derived<int>::foo()f(d2); // 调用Derived<double>::foo()return 0;
}
-
我们定义了一个函数对象 Functor,其中的 operator() 是一个模板函数。函数对象通过调用 obj.foo() 来执行对象的 foo() 方法,并在控制台上打印相关信息。Derived 类是一个模板类,它继承自 Base 类,实现了 foo() 方法。
-
在 main() 函数中,我们创建了两个不同类型的 Derived 对象 d1 和 d2,然后创建了一个 Functor 对象 f。通过调用 f(d1) 和 f(d2),我们将不同类型的对象传递给函数对象 f,它将根据对象的类型调用适当的 foo() 实现。
使用std::function
- 通过使用 std::function,我们可以将不同类型的可调用对象进行类型擦除,并以一致的方式进行处理。
#include <iostream>
#include <functional>struct Base {virtual void foo() const = 0;
};struct Derived1 : public Base {void foo() const override {std::cout << "Derived1::foo()" << std::endl;}
};struct Derived2 : public Base {void foo() const override {std::cout << "Derived2::foo()" << std::endl;}
};void process(const std::function<void()>& func) {func(); // 调用适当的函数实现
}int main() {Derived1 d1;Derived2 d2;std::function<void()> func1 = [&d1]() { d1.foo(); };std::function<void()> func2 = [&d2]() { d2.foo(); };process(func1); // 调用Derived1::foo()process(func2); // 调用Derived2::foo()return 0;
}
-
在这个示例中,我们定义了 Base 类和两个派生类 Derived1 和 Derived2。Base 类有一个纯虚函数 foo(),每个派生类都提供了自己的实现。
-
process() 函数接受一个 std::function<void()> 类型的参数,它表示一个无返回值、不带参数的可调用对象。通过传递不同的 std::function 对象给 process() 函数,我们可以在运行时选择适当的函数实现。
进一步,使用std::bind
- 使用 std::bind 可以实现类型擦除和延迟绑定,允许在运行时选择函数实现,并提供具体的参数值。
#include <iostream>
#include <functional>struct Base {virtual void foo(int value) const = 0;
};struct Derived1 : public Base {void foo(int value) const override {std::cout << "Derived1::foo(" << value << ")" << std::endl;}
};struct Derived2 : public Base {void foo(int value) const override {std::cout << "Derived2::foo(" << value << ")" << std::endl;}
};void process(const std::function<void()>& func) {func(); // 调用适当的函数实现
}int main() {Derived1 d1;Derived2 d2;auto func1 = std::bind(&Derived1::foo, &d1, 42);auto func2 = std::bind(&Derived2::foo, &d2, 24);process(func1); // 调用Derived1::foo(42)process(func2); // 调用Derived2::foo(24)return 0;
}
- 在示例中,func1 绑定了 Derived1::foo 成员函数,并提供了一个参数值 42。同样地,func2 绑定了 Derived2::foo 成员函数,并提供了一个参数值 24。通过调用 process() 函数,我们可以分别调用适当的函数实现。
二、任务队列
1.基于特定类型的方式实现
假设任务类如下所示:
//任务队列的类型是my_queue<std::unique_ptr<task_base>>,用基类指针去管理任务对象
class my_thread {using task_type = void(*)();my_queue<std::unique_ptr<task_base>> task_queue;//处理不同子类对象的run()的逻辑,可能实现void Loop() noexcept{for(auto& task: task_queue){task->run();}}
}; // 假设具体的任务函数体的调用签名都是void
struct task_base {virtual ~task_base() = 0;virtual void run() const = 0;
};// 用户编写的具体任务类
struct task_impl : public task_base { void run() const override {// 运算...}
};
优点:容易实现
缺点:非常缺乏伸缩性。
- 首先,编写子类的责任被推给了用户,可能一个不太复杂的函数调用会被强加上任务基类task_base的包装;
- 而且用起来也不方便。
2.基于任意类型的方式实现
使用类型擦除技术,这类设施典型的代表就是std::function,它通过类型擦除的技巧,不必麻烦用户编写继承相关代码,并能包装任意的函数对象。
C++语境下的类型擦除,技术上来说,是编写一个类,它提供模板的构造函数和非虚函数接口提供功能;隐藏了对象的具体类型,但保留其行为。
- 简单地说,就是库作者把面向对象的代码写了,而不是推给用户写:
- 首先,抽象基类task_base作为公共接口不变;
- 其子类task_model(角色同上文中的task_impl)写成类模板的形式,其把一个任意类型F的函数对象function_作为数据成员。
- 子类写成类模板的具体用意是,对于用户提供的一个任意的类型F,F不需要知道task_base及其继承体系,而只进行语法上的duck typing检查。 这种方法避免了继承带来的侵入式设计。 换句话说,只要能合乎语法地对F调用预先定义的接口,代码就可以编译,这个技巧就能运作。
- 此例中,预先定义的接口是void(),以functor_();的形式调用。
struct task_base {virtual ~task_base() {}virtual void operator()() const = 0;
};template <typename F>
struct task_model : public task_base {F functor_;template <typename U> // 构造函数是函数模板task_model(U&& f) :functor_(std::forward<U>(f)) {}void operator()() const override {functor_();}
};
然后,我们把它包装起来:
- 首先,初始动机是用一个类型包装不同的函数对象。
- 然后,考虑这些函数对象需要提供的功能(affordance),此处为使用括号运算符进行函数调用。
- 最后,把这个功能抽取为一个接口,此处为my_task,我们在在这一步擦除了对象具体的类型。
- 这便是类型擦除的本质:切割类型与其行为,使得不同的类型能用同一个接口提供功能。
class my_task {std::unique_ptr<task_base> ptr_;public:template <typename F>my_task(F&& f) {using model_type = task_model<F>;ptr_ = std::make_unique<model_type>(std::forward<F>(f)); }void operator()() const {ptr_->operator()();} // 移动构造函数my_task(my_task&& oth) noexcept : ptr_(std::move(oth.ptr_)){}// 移动赋值函数my_task& operator=(my_task&& rhs) noexcept {ptr_ = std::move(rhs.ptr_);return *this;}
};class my_thread {using task_type = void(*)();my_queue<my_task> task_queue;//处理不同子类对象的run()的逻辑,可能实现void Loop() noexcept{for(auto& task: task_queue){task();}}
};
测试:
对my_task进行简单测试的代码如下:
- 其实完全可以用std::function代替my_task,来实现类型擦除,这样连虚函数都不需要了;如果采用虚函数的方式,可以参考1和2的方法去设计
// 普通函数
void foo() {std::cout << "type erasure 1";
}
my_task t1{ &foo };
t1(); // 输出"type erasure 1"// 重载括号运算符的类
struct foo2 {void operator()() {std::cout << "type erasure 2";}
};
my_task t2{ foo2{} };
t2(); // 输出"type erasure 2"// Lambda
my_task t3{[](){ std::cout << "type erasure 3"; }
};
t3(); // 输出"type erasure 3"
总结:
- 第一层是task_base。考虑需要的功能后,以虚函数的形式提供对应的接口I。
- 第二层是task_model。这是一个类模板,用来存放用户提供的类T,T应当语法上满足接口I;重写task_base的虚函数,在虚函数中调用T对应的函数。
- 第三层是对应my_task。存放一个task_base指针p指向task_model对象m;拥有一个模板构造函数,以适应任意的用户提供类型;以非虚函数的形式提供接口I,通过p调用m。
上述可能存在的问题1:
my_task t1{ &foo1 };/*
foo作为参数传递给一个函数模板时,会被“准确”地推断为函数类型void(),而不是函数指针类型void(*)(
*/
my_task t2{ foo1 }; // 编译出错,
-
解决办法1:简单的解决方法是,每次都记得用取地址运算符&
-
解决办法2:让模板将函数类型推导为函数指针类型void(*)(),修改my_task的构造函数为:
class my_task {template <typename F>my_task(F&& f) {// 使用std::decay来显式地进行类型退化// 如果传入函数类型就退化为函数指针类型using F_decay = std::decay_t<F>;using model_type = task_model<F_decay>; ptr_ = std::make_unique<model_type>(std::forward<F_decay>(f));}
};
模板元编程就是在编译时进行运算并生成代码的代码(所谓“元”)
上述可能存在的问题2:
// 复制构造my_task
my_task t1{[]() { std::cout << "type erasure"; }
};/*
事实上,如果这样去构造t2,编译器不会报错,但是运行时会栈溢出!如果查看栈记录,会发现程序一直在my_task的构造函数和task_model构造函数之间无限循环。Word?
*/
my_task t2{ t1 }; // 发生了什么?
从编译期函数解析的角度,分析这段代码可以通过编译的原因:
从t1复制构造t2时,编译器的第一选择是my_task的复制构造函数,但它被禁用了;
于是,编译器退而求其次地尝试匹配my_task的第一个构造函数,template my_task(F&&)。
而这个构造函数并没有限制F不能为my_task, 编译器就选择调用它。所以,这段代码可以过编译。
解决办法:
- 禁止my_task的模板构造函数的类型参数F为my_task
template <typename F>
using is_not_my_task = std::enable_if_t<!std::is_same_v< std::remove_cvref_t<F>, my_task >,int>;template <typename F, is_not_my_task<F> = 0>
my_task(F&& f);使用C++20 Concept
template <typename F>
concept is_not_my_task = !std::is_same_v<std::remove_cvref_t<F>, my_task>;class my_task {template <typename F> requires is_not_my_task<F>my_task(F&& f);
};
参考:
- 深入浅出C++类型擦除(1)
- 深入浅出C++类型擦除(2)
相关文章:
C++中的类型擦除技术
文章目录 一、C类型擦除Type Erasure技术1.虚函数2.模板和函数对象 二、任务队列1.基于特定类型的方式实现2.基于任意类型的方式实现 参考: 一、C类型擦除Type Erasure技术 C中的类型擦除(Type Erasure)是一种技术,用于隐藏具体类…...
激光雷达 01 线数
一、线数 对于 360 旋转式和一维转镜式架构的激光雷达来说,有几组激光收发模块,垂直方向上就有几条线,被称为线数。这种情况下,线数就等同于激光雷达内部激光器的数量[参考]。 通俗来讲,线数越高,激光器的…...
PHP 公交公司充电桩管理系统mysql数据库web结构apache计算机软件工程网页wamp
一、源码特点 PHP 公交公司充电桩管理系统是一套完善的web设计系统,对理解php编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。 源码下载 https://download.csdn.net/download/qq_41221322/88220946 论文下…...
HTML <strong> 标签
定义和用法 以下元素都是短语元素。虽然这些标签定义的文本大多会呈现出特殊的样式,但实际上,这些标签都拥有确切的语义。 我们并不反对使用它们,但是如果您只是为了达到某种视觉效果而使用这些标签的话,我们建议您使用样式表&a…...
机器学习笔记 - 使用 ResNet-50 和余弦相似度的基于图像的推荐系统
一、简述 这里的代码主要是基于图像的推荐系统,该系统利用 ResNet-50 深度学习模型作为特征提取器,并采用余弦相似度来查找给定输入图像的最相似嵌入。 该系统旨在根据所提供图像的视觉内容为用户提供个性化推荐。 二、所需环境 Python 3.x tensorflow ==2.5.0 numpy==1.21.…...
Codeforces Round 881 Div.3
文章目录 贪心:A. Sasha and Array Coloring结论:B. Long Long性质:C. Sum in Binary Treedfs求叶子数量:D. Apple Tree二分与前缀和:E. Tracking Segments 贪心:A. Sasha and Array Coloring Problem - A…...
Kubernetes(K8s)从入门到精通系列之十六:linux服务器安装minikube的详细步骤
Kubernetes K8s从入门到精通系列之十六:linux服务器安装minikube的详细步骤 一、安装Docker二、创建启动minikube用户三、下载minikube四、安装minikube五、将minikube命令添加到环境变量六、启动minikube七、查看pods,自动安装kubectl八、重命名minikube kubectl --为kubect…...
JDBC配置文件抽取-spring11
改成context,到这里我们context命名空间就引入完毕,加载我们外部properties配置文件: 用它:第一个属性,第二个类型 在未加载路径下: 现在我已经把spring加载到配置文件里了。 现在我需要在这个位置引入proper…...
el-form组件相关的一些基础使用
el-checkbox 01.description 多选单选框 02.场景举例 需要对每一条数据展示她的某些状态是否存在 03.代码展示 <el-checkbox v-model"query.isAutoAccptncsign" true-label1 false-label0 :disabled"ifReview?true:false">自动发起承兑应答</…...
全新 – Amazon EC2 M1 Mac 实例
去年,在 re: Invent 2021 大会期间,我写了一篇博客文章,宣布推出 EC2 M1 Mac 实例的预览版。我知道你们当中许多人请求访问预览版,我们尽了最大努力,却无法让所有人满意。不过,大家现在已经无需等待了。我很…...
java # Servlet
一、什么是Servlet? Servlet是javaEE规范之一。规范就是接口。JavaWeb三大组件分别是:Servlet程序、Filter过滤器、Listener监听器。Servlet是运行在服务器上的一个Java小程序,它可以接收客户端发送来的请求,并响应数据给客户端。…...
Linux内核的两种安全策略:基于inode的安全与基于文件路径的安全
实现系统安全的策略 在Linux中,一切且为文件,实现系统安全的策略主要可分为两种:基于inode的安全、基于文件路径的安全。 基于inode的安全 为文件引入安全属性,安全属性不属于文件内容,它是文件的元数据,…...
有哪些前端开发工具推荐? - 易智编译EaseEditing
在前端开发中,有许多工具可以帮助你更高效地进行开发、调试和优化。以下是一些常用的前端开发工具推荐: 代码编辑器/集成开发环境(IDE): Visual Studio Code:功能强大、轻量级的代码编辑器,支…...
【JAVA】抽象类与接口
⭐ 作者:小胡_不糊涂 🌱 作者主页:小胡_不糊涂的个人主页 📀 收录专栏:浅谈Java 💖 持续更文,关注博主少走弯路,谢谢大家支持 💖 抽象类与接口 1. 抽象类1.1 抽象类的概念…...
人脸图像处理
1,人脸图像与特征基础 人脸图像的特点 规律性: 人的两只眼睛总是对称分布在人脸的上半部分,鼻子和嘴唇中心点的连线基本与两眼之间的连线垂直,嘴绝对不会超过眼镜的两端点(双眼为d,则双眼到嘴巴的垂直距离一般在0.8-1.25) 唯一性 非侵扰与便利性 可扩展性 人脸图像的应用 身份…...
Docker入门——实战图像分类
一、背景 思考: 在一个项目的部署阶段,往往需要部署到云服务器或者是终端设备上,而环境的搭建往往是最费时间和精力的,特别是需要保证运行环境一致性,有什么办法可以批量部署相同环境呢? Docker本质——…...
【HarmonyOS北向开发】-02 第一个程序测试
飞书原文档链接:Docs...
关于小程序收集用户手机号行为的规范
手机号在日常生活中被广泛使用,是重要的用户个人信息,小程序开发者应在用户明确同意的前提下,依法合规地处理用户的手机号信息。 而部分开发者在处理用户手机号过程中,存在不规范收集行为,影响了用户的正常使用体验&a…...
js判断手指的上滑,下滑,左滑,右滑,事件监听 和 判断鼠标滚轮向上滚动滑轮向下滚动
js判断手指的上滑,下滑,左滑,右滑,事件监听 和 判断鼠标滚轮向上滚动滑轮向下滚动 pc端 判断鼠标滚轮向上滚动滑轮向下滚动 const scrollFunc (e) > { e e || window.event; let wheelDelta e.wheelDelta ? e.wheelDelta…...
ES 一些简单 的查询注意事项
term query 不分词字段 带分数 where namexxx filter 分词字段 不分词字段 不带分数 Terms query 所有类型 带分数 where name in(xxx) Range query where name between xxx and xxx Exists Regexp Match query 分词字段/基础字段 Multi-match query 多个分词字段/基础字段 Boo…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...
C++:多态机制详解
目录 一. 多态的概念 1.静态多态(编译时多态) 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1).协变 2).析构函数的重写 5.override 和 final关键字 1&#…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...
解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...
SQL Server 触发器调用存储过程实现发送 HTTP 请求
文章目录 需求分析解决第 1 步:前置条件,启用 OLE 自动化方式 1:使用 SQL 实现启用 OLE 自动化方式 2:Sql Server 2005启动OLE自动化方式 3:Sql Server 2008启动OLE自动化第 2 步:创建存储过程第 3 步:创建触发器扩展 - 如何调试?第 1 步:登录 SQL Server 2008第 2 步…...
