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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/42622.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Electron基础篇

人生有些事,错过一时,就错过一世。 官网&#xff1a;简介 | Electron Electron-大多用来写桌面端软件 Electron介绍 Electront的核心组成是Chromium、Node.js以及内置的Native API&#xff0c;其中Chromium为Electron提供强大的UI能力&#xff0c;可以在不考虑兼容的情况下利…

使用神卓互联内网穿透搭建远程访问公司ERP系统

神卓互联是一款企业级内网穿透软件&#xff0c;可以将内网中的服务映射到公网上&#xff0c;实现内网服务的访问。通过神卓互联&#xff0c;您可以远程访问ERP系统。在使用神卓互联进行内网穿透时&#xff0c;您只需要在生成的公网地址后面加上ERP系统的端口号&#xff0c;即可…

NVIDIA vGPU License许可服务器高可用全套部署秘籍

第1章 前言 近期遇到比较多的场景使用vGPU&#xff0c;比如Citrix 3D场景、Horizon 3D场景&#xff0c;还有AI等&#xff0c;都需要使用显卡设计研发等&#xff0c;此时许可服务器尤为重要&#xff0c;许可断掉会出现掉帧等情况&#xff0c;我们此次教大家部署HA许可服务器。 …

【.net】本地调试运行只能用localhost的问题

【.net】本地调试运行只能用localhost的问题 解决方案 找到到项目目录下 隐藏文件夹 .vs /项目名称/config/applicationhost.config <bindings><binding protocol"http" bindingInformation"*:1738:localhost" /></bindings> 再加一条你…

职业学院物联网实训室建设方案

一、概述 1.1专业背景 物联网&#xff08;Internet of Things&#xff09;被称为继计算机、互联网之后世界信息产业第三次浪潮&#xff0c;它并非一个全新的技术领域&#xff0c;而是现代信息技术发展到一定阶段后出现的一种聚合性应用与技术提升&#xff0c;是随着传感网、通…

如何判断自己是否适合游戏开发?

引言 游戏开发是一个充满创意和技术挑战的领域&#xff0c;吸引着越来越多的年轻人投身其中。然而&#xff0c;要想在游戏开发领域获得成功&#xff0c;首先需要明确自己是否适合这个领域。本文将为你介绍一些判断自己是否适合游戏开发的关键因素。 1. 技术兴趣和编程能力 游…

Python 程序设计入门(024)—— Python 的文件操作

Python 程序设计入门&#xff08;024&#xff09;—— Python 的文件操作 目录 Python 程序设计入门&#xff08;024&#xff09;—— Python 的文件操作一、文件对象二、读取文件内容的方法1、read() 方法2、readline() 方法3、readlines() 方法4、使用 for 循环读取文件内容 …

麦肯锡发布《2023科技趋势展望报告》,生成式AI、下一代软件开发成为趋势,软件测试如何贴合趋势?

近日&#xff0c;麦肯锡公司发布了《2023科技趋势展望报告》。报告列出了15个趋势&#xff0c;并把他们分为5大类&#xff0c;人工智能革命、构建数字未来、计算和连接的前沿、尖端工程技术和可持续发展。 类别一&#xff1a;人工智能革命 生成式AI 生成型人工智能标志着人工智…

CSRF

文章目录 CSRF(get)CSRF(post)CSRF Token CSRF(get) 根据提示的用户信息登录 点击修改个人信息 开启bp代理&#xff0c;点击submit 拦截到请求数据包 浏览器关闭代理 刷新页面 CSRF(post) 使用BP生成CSRF POC post请求伪造&#xff0c;可以通过钓鱼网站&#xff0c;诱导用户去…

docker 常用命令大全

1.查看docker版本&#xff1a; docker -v2.检查 Docker 是否正在运行: systemctl status docker3.重启docker服务: systemctl restart docker4.列出本地镜像: docker images5.列出正在运行的容器&#xff1a; docker ps6.列出所有容器&#xff08;包括停止的&#xff09;&…

css 实现文字横向循环滚动

实现效果 思路 ## 直接上代码,html部分 //我这里是用的uniapp <view class"weather_info_wrap"><view class"weather_info">当前多云&#xff0c;今晚8点转晴&#xff0c;明天有雨&#xff0c;温度32摄氏度。</view><view class&qu…

CF1005A Tanya and Stairways 题解

题目传送门 题目意思&#xff1a; 给你 n n n 个数&#xff0c;如果第 i i i 个数小于或等于第 i − 1 i-1 i−1 个数&#xff0c;就输出这个数。 思路&#xff1a; 输入后直接遍历判断即可。 代码&#xff1a; #include<bits/stdc.h> using namespace std; int …

解决IDEA tomcat控制台只有server日志

解决IDEA tomcat控制台只有server日志 确认tomcatxxx/conf/logging.properties文件是否存在&#xff0c;存在就会有。前提是在run configuration配置了打印多个日志

uniapp封装组件,选中后右上角显示对号√样式(通过css实现)

效果&#xff1a; 一、组件封装 1、在项目根目录下创建components文件夹&#xff0c;自定义组件名称&#xff0c;我定义的是xc-button 2、封装组件代码 <template><view class"handle-btn"><view :class"handleIdCode 1 ? select : unSelec…

蚂蚁数科持续发力PaaS领域,SOFAStack布局全栈软件供应链安全产品

8月18日&#xff0c;记者了解到&#xff0c;蚂蚁数科再度加码云原生PaaS领域&#xff0c;SOFAStack率先完成全栈软件供应链安全产品及解决方案的布局&#xff0c;包括静态代码扫描Pinpoint、软件成分分析SCA、交互式安全测试IAST、运行时防护RASP、安全洞察Appinsight等&#x…

【电商领域】Axure在线购物商城小程序原型图,品牌自营垂直电商APP原型

作品概况 页面数量&#xff1a;共 60 页 兼容软件&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 应用领域&#xff1a;网上商城、品牌自营商城、商城模块插件 作品申明&#xff1a;页面内容仅用于功能演示&#xff0c;无实际功能 作品特色 本作品为品牌自营网上商城…

无涯教程-Perl - warn函数

描述 此函数将LIST的值打印到STDERR。基本上与die函数相同,除了不对出口进行任何调用并且在eval语句内不引发异常。这对于引发错误而不导致脚本过早终止很有用。 如果变量$包含一个值(来自先前的eval调用),并且LIST为空,则$的值将以。\t.caught打印。附加到末尾。如果$和LIST…

MySQL数据库概述

MySQL数据库概述 1 SQL SQL语句大小写不敏感。 SQL语句末尾应该使用分号结束。 1.1 SQL语句及相关操作示例 DDL&#xff1a;数据定义语言&#xff0c;负责数据库定义、数据库对象定义&#xff0c;由CREATE、ALTER与DROP三个语法所组成DML&#xff1a;数据操作语言&#xff…

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

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

ElasticSearchConfig

1. 添加配置 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId></dependency>2. es 配置信息 import org.apache.http.HttpHost; import org.apache.http.auth.Au…