当前位置: 首页 > news >正文

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.基于任意类型的方式实现 参考&#xff1a; 一、C类型擦除Type Erasure技术 C中的类型擦除&#xff08;Type Erasure&#xff09;是一种技术&#xff0c;用于隐藏具体类…...

激光雷达 01 线数

一、线数 对于 360 旋转式和一维转镜式架构的激光雷达来说&#xff0c;有几组激光收发模块&#xff0c;垂直方向上就有几条线&#xff0c;被称为线数。这种情况下&#xff0c;线数就等同于激光雷达内部激光器的数量[参考]。 通俗来讲&#xff0c;线数越高&#xff0c;激光器的…...

PHP 公交公司充电桩管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 公交公司充电桩管理系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 源码下载 https://download.csdn.net/download/qq_41221322/88220946 论文下…...

HTML <strong> 标签

定义和用法 以下元素都是短语元素。虽然这些标签定义的文本大多会呈现出特殊的样式&#xff0c;但实际上&#xff0c;这些标签都拥有确切的语义。 我们并不反对使用它们&#xff0c;但是如果您只是为了达到某种视觉效果而使用这些标签的话&#xff0c;我们建议您使用样式表&a…...

机器学习笔记 - 使用 ResNet-50 和余弦相似度的基于图像的推荐系统

一、简述 这里的代码主要是基于图像的推荐系统,该系统利用 ResNet-50 深度学习模型作为特征提取器,并采用余弦相似度来查找给定输入图像的最相似嵌入。 该系统旨在根据所提供图像的视觉内容为用户提供个性化推荐。 二、所需环境 Python 3.x tensorflow ==2.5.0 numpy==1.21.…...

Codeforces Round 881 Div.3

文章目录 贪心&#xff1a;A. Sasha and Array Coloring结论&#xff1a;B. Long Long性质&#xff1a;C. Sum in Binary Treedfs求叶子数量&#xff1a;D. Apple Tree二分与前缀和&#xff1a;E. Tracking Segments 贪心&#xff1a;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命名空间就引入完毕&#xff0c;加载我们外部properties配置文件&#xff1a; 用它&#xff1a;第一个属性&#xff0c;第二个类型 在未加载路径下&#xff1a; 现在我已经把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 实例

去年&#xff0c;在 re: Invent 2021 大会期间&#xff0c;我写了一篇博客文章&#xff0c;宣布推出 EC2 M1 Mac 实例的预览版。我知道你们当中许多人请求访问预览版&#xff0c;我们尽了最大努力&#xff0c;却无法让所有人满意。不过&#xff0c;大家现在已经无需等待了。我很…...

java # Servlet

一、什么是Servlet&#xff1f; Servlet是javaEE规范之一。规范就是接口。JavaWeb三大组件分别是&#xff1a;Servlet程序、Filter过滤器、Listener监听器。Servlet是运行在服务器上的一个Java小程序&#xff0c;它可以接收客户端发送来的请求&#xff0c;并响应数据给客户端。…...

Linux内核的两种安全策略:基于inode的安全与基于文件路径的安全

实现系统安全的策略 在Linux中&#xff0c;一切且为文件&#xff0c;实现系统安全的策略主要可分为两种&#xff1a;基于inode的安全、基于文件路径的安全。 基于inode的安全 为文件引入安全属性&#xff0c;安全属性不属于文件内容&#xff0c;它是文件的元数据&#xff0c…...

有哪些前端开发工具推荐? - 易智编译EaseEditing

在前端开发中&#xff0c;有许多工具可以帮助你更高效地进行开发、调试和优化。以下是一些常用的前端开发工具推荐&#xff1a; 代码编辑器/集成开发环境&#xff08;IDE&#xff09;&#xff1a; Visual Studio Code&#xff1a;功能强大、轻量级的代码编辑器&#xff0c;支…...

【JAVA】抽象类与接口

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;浅谈Java &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; 抽象类与接口 1. 抽象类1.1 抽象类的概念…...

人脸图像处理

1,人脸图像与特征基础 人脸图像的特点 规律性: 人的两只眼睛总是对称分布在人脸的上半部分,鼻子和嘴唇中心点的连线基本与两眼之间的连线垂直,嘴绝对不会超过眼镜的两端点(双眼为d,则双眼到嘴巴的垂直距离一般在0.8-1.25) 唯一性 非侵扰与便利性 可扩展性 人脸图像的应用 身份…...

Docker入门——实战图像分类

一、背景 思考&#xff1a; 在一个项目的部署阶段&#xff0c;往往需要部署到云服务器或者是终端设备上&#xff0c;而环境的搭建往往是最费时间和精力的&#xff0c;特别是需要保证运行环境一致性&#xff0c;有什么办法可以批量部署相同环境呢&#xff1f; Docker本质——…...

【HarmonyOS北向开发】-02 第一个程序测试

飞书原文档链接&#xff1a;Docs...

关于小程序收集用户手机号行为的规范

手机号在日常生活中被广泛使用&#xff0c;是重要的用户个人信息&#xff0c;小程序开发者应在用户明确同意的前提下&#xff0c;依法合规地处理用户的手机号信息。 而部分开发者在处理用户手机号过程中&#xff0c;存在不规范收集行为&#xff0c;影响了用户的正常使用体验&a…...

js判断手指的上滑,下滑,左滑,右滑,事件监听 和 判断鼠标滚轮向上滚动滑轮向下滚动

js判断手指的上滑&#xff0c;下滑&#xff0c;左滑&#xff0c;右滑&#xff0c;事件监听 和 判断鼠标滚轮向上滚动滑轮向下滚动 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…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?

大家好&#xff0c;欢迎来到《云原生核心技术》系列的第七篇&#xff01; 在上一篇&#xff0c;我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在&#xff0c;我们就像一个拥有了一块崭新数字土地的农场主&#xff0c;是时…...

智慧医疗能源事业线深度画像分析(上)

引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

Spring Boot 实现流式响应(兼容 2.7.x)

在实际开发中&#xff0c;我们可能会遇到一些流式数据处理的场景&#xff0c;比如接收来自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 内容&#xff0c;并将其原样中转给前端页面或客户端。这种情况下&#xff0c;传统的 RestTemplate 缓存机制会…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

测试markdown--肇兴

day1&#xff1a; 1、去程&#xff1a;7:04 --11:32高铁 高铁右转上售票大厅2楼&#xff0c;穿过候车厅下一楼&#xff0c;上大巴车 &#xffe5;10/人 **2、到达&#xff1a;**12点多到达寨子&#xff0c;买门票&#xff0c;美团/抖音&#xff1a;&#xffe5;78人 3、中饭&a…...

现代密码学 | 椭圆曲线密码学—附py代码

Elliptic Curve Cryptography 椭圆曲线密码学&#xff08;ECC&#xff09;是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础&#xff0c;例如椭圆曲线数字签…...

Web 架构之 CDN 加速原理与落地实践

文章目录 一、思维导图二、正文内容&#xff08;一&#xff09;CDN 基础概念1. 定义2. 组成部分 &#xff08;二&#xff09;CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 &#xff08;三&#xff09;CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 &#xf…...

LeetCode - 199. 二叉树的右视图

题目 199. 二叉树的右视图 - 力扣&#xff08;LeetCode&#xff09; 思路 右视图是指从树的右侧看&#xff0c;对于每一层&#xff0c;只能看到该层最右边的节点。实现思路是&#xff1a; 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

【生成模型】视频生成论文调研

工作清单 上游应用方向&#xff1a;控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...