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…...
XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...
1.3 VSCode安装与环境配置
进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件,然后打开终端,进入下载文件夹,键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...
基于数字孪生的水厂可视化平台建设:架构与实践
分享大纲: 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年,数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段,基于数字孪生的水厂可视化平台的…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
