跟我学c++中级篇——类型擦除的应用

news/2024/7/24 12:04:53 标签: c++, 开发语言

一、类型擦除的效果

前面分析了类型擦除,通过分析可以知道。类型擦除其实就是一种抽象,不通过继承来实现动态行为,从而更好的实现可扩展性和耦合性。在设计程序时,大家都知道,要依赖于抽象而不是具体。由于类型的限制,在c++这种强类型语言中,往往会造成抽象的复杂性,甚至在某些情况下是无法达到抽象的结果的。
特别在模板编程中,这种现象往往更是突出,所以类型擦除在这方面有着很大的施展空间。

二、例程及分析

在这里首先分析一个线程任务的队列控制例程,通过这个例程可以更好的明白类型擦除的用处。一般来说,任务就是一个函数,或者可以理解成一个实现函数功能的对象。通过前面的学习,就可以知道了,主要有三大类,普通函数(含函数指针)、仿函数和lambda表达式。在摒除一些细节后,如何把这三类统一起来就是一种抽象。在早期的开发中,一般都是只考虑一类,大多都是使用函数指针,在c++11推出以后,更多的开发者开始使用std::function,因为它把三类也给统一了。下面看一个简单的例程,形象的理解一下:

#include <iostream>
#include <memory>
#include <queue>

//基础应用
typedef void(*pFtask)();
void WorkTask()
{
    std::cout << "do work" << std::endl;
}
class BaseTask
{
public:
    BaseTask() = default;
    virtual ~BaseTask() = default;
public:
    virtual void DoWork() const = 0;
};
class TaskImpl :public BaseTask
{
public:
    TaskImpl() = default;
    ~TaskImpl() = default;
public:
    void DoWork()const override
    {
        WorkTask();
    }
};
class ThreadWork
{
public:
    ThreadWork() = default;
    ~ThreadWork() = default;
public:
public:
    std::queue<std::unique_ptr<BaseTask>> qTask_;
};
int main()
{
    //make_unique是c++14才提供,这里暂时用主线程来模拟线程
    std::unique_ptr<BaseTask> pTask = std::make_unique<TaskImpl>();
    ThreadWork tw;
    tw.qTask_.emplace(std::move(pTask));

    //仍然使用主线程模拟任务执行
    auto pT = std::move(tw.qTask_.front());
    tw.qTask_.pop();

    pT->DoWork();
    system("pause");
}

下面就从上面这个最初的代码一步步的扩展开去,看看类型擦除在这上面是如何应用的。上面的代码其实对于一些内部使用的线程操作基本没有什么问题了,再完善一下队列,增加一个内存池,这就是一个基本可用的线程池的整体模型。
但是这样有一些问题,第一,每次实现不同的任务或者不同的开发者实现自己的任务都需要继承基础的抽象类,这里先不谈效率,时间长后,对维护本身就非常不友好;第二,使用者无法屏蔽对代码的抽象,仍然需要了解代码,即使这个代码很简单,同样这种抽象也限制了任务的扩展。
另外,如果想使用仿函数和lambda表达式,又该怎么样?能不能有一种情况,让客户拿来任务数据结构直接就使用,只实现任务函数即类似于下面的这样:

//定义
class WorkTask
{
public:
   //函数对象管理
   template <typename F>
   WorkTask(F&& f):f_(std::move(f));
public:
    void operator()()const;
private:
    F f_;
};

//使用
WorkTask wTask{/*用户自定义的任务执行函数对象,要支持函数指针、lambda表达式和仿函数*/};
wTask();

如果这样使用,对应用用户就友好了很多,编程的维护复杂性也大大降低。怎么实现这个呢?先看一下代码:

class BaseTask
{
public:
    BaseTask() = default;
    virtual ~BaseTask() = default;
public:
    virtual void DoWork() const = 0;
    virtual void operator()()const = 0;
};
template<typename F>
class TaskImpl :public BaseTask
{
public:
    TaskImpl() = default;
    template<typename T>
    TaskImpl(T&& t) :func_(std::forward<T>(t)) {}
    ~TaskImpl() = default;
public:
    void DoWork()const override
    {
        //WorkTask();
        std::cout << "start task!" << std::endl;
    }
    void operator()()const override
    {
        func_();
    }

public:
    F func_;
};

在上面的代码中,首先增加了对小括号的重载,重载的目的当然就是对仿函数的支持。在前面的“类型擦除”分析中,可以知道,函数指针和受约束的模板构造函数是实现类型擦除的关键。从上面的代码看,抛除对约束的控制,函数指针(包含仿函数)就是首要(func_),基本的任务体TaskImpl采用了模板类和模板构造函数。好多技术在学习的过程中,往往是只重点对某一方向进行分析,但较少的是前后响应,互相借鉴,这并不是说写文章的人水平不够,而是说很多写文章的人往往忽视了这一点,陷入了技术细节的分析和描述。而这也往往是很多人学会一个单独的技巧或者技术后不能够应用于实际场景的一个非常重要的原因。扯远了,再扯回来。
上面的代码解决了类型擦除的基本问题,也印证了前面说的如何实现类型擦除的手段。但是这样用,仍然有一些暴露细节,所以需要再封装一层:

class TaskWrapper
{
public:
    TaskWrapper() = default;
    template<typename F>
    TaskWrapper(F &&f)
    {
        using standType =  TaskImpl<F>;
        pTask_ = std::make_unique<standType>(std::forward<F>(f));
    }
    ~TaskWrapper() = default;
public:
    void operator()()const 
    {
        pTask_->operator()();
        //pTask_->DoWork();
    }
public:
    std::unique_ptr<BaseTask> pTask_;
};

通过对基本任务类的封装,并且进行仿函数的operator重载,这样,就可以将三类函数对象的支持统一到此类中。对其进行测试:

void WorkTask()
{
    std::cout << "do work,type erase" << std::endl;
}

class WorkTaskFunc
{
public:
    void operator()()const 
    {
        std::cout << "functor  ,type erase" << std::endl;
    }
};

auto tasklambda = []() {std::cout << "lambda,type erase" << std::endl; };
int main()
{
    //分三种情况
    //普通函数
    TaskWrapper tw1{ WorkTask };
    tw1();

    //仿函数
    TaskWrapper tw2{ WorkTaskFunc{} };
    tw2();

    //Lambda表达式
    TaskWrapper tw3{ tasklambda };
    tw3();

    return 0;
}

然后再象开始一样把代码集成到ThreadWork中:

void TestTask()
{
    //分三种情况
    //普通函数
    TaskWrapper tw1{ WorkTask };
    tw1();

    //仿函数
    TaskWrapper tw2{ WorkTaskFunc{} };
    tw2();

    //Lambda表达式
    TaskWrapper tw3{ tasklambda };
    tw3();

    ThreadWork tWork;
    tWork.qW_.emplace(std::move(tw1));
    tWork.qW_.emplace(std::move(tw2));
    tWork.qW_.emplace(std::move(tw3));

    auto t1 = std::move(tWork.qW_.front());
    tWork.qW_.pop();

    auto t2 = std::move(tWork.qW_.front());
    tWork.qW_.pop();

    auto t3 = std::move(tWork.qW_.front());
    tWork.qW_.pop();

    t1();
    t2();
    t3();
}

程序一运行就崩溃了,报得错误是在执行t1()时,资源被释放,TaskWrapper中的pTask_已经被释放。原来此处用的是std::unique_ptr,它只能移动不能拷贝,于是就得对移动构造函数和移动复制函数进行显示声明,同时处理掉复制相关的函数:

class TaskWrapper
{
......
public:
    TaskWrapper(TaskWrapper&& other)noexcept :pTask_(std::move(other.pTask_)) {}
    TaskWrapper& operator = (TaskWrapper&& rhs)noexcept
    {
        pTask_ = std::move(rhs.pTask_);
        return *this;
    }
    //处理复制构造、赋值函数
    TaskWrapper(const TaskWrapper&) = delete;
    TaskWrapper& operator=(const TaskWrapper&) = delete;
......
};

最后再统一到队列中,看完整的代码:

#include <iostream>
#include <memory>
#include <utility>
#include <queue>

//基础应用
typedef void(*pFtask)();
//普通函数
void WorkTask()
{
    std::cout << "do work,type erase" << std::endl;
}
//仿函数
class WorkTaskFunc
{
public:
    void operator()()const 
    {
        std::cout << "functor  ,type erase" << std::endl;
    }
};
//lambda表达式
auto tasklambda = []() {std::cout << "lambda,type erase" << std::endl; };

//任务基础抽象类
class BaseTask
{
public:
    BaseTask() = default;
    virtual ~BaseTask() = default;
public:
    virtual void DoWork() const = 0;
    virtual void operator()()const = 0;
};
//标准任务类
template<typename F>
class TaskImpl :public BaseTask
{
public:
    TaskImpl() = default;
    template<typename T>
    TaskImpl(T&& t) :func_(std::forward<T>(t)) {}
    ~TaskImpl() = default;
public:
    void DoWork()const override
    {
        //WorkTask();
        std::cout << "start task!" << std::endl;
    }
    void operator()()const override
    {
        func_();
    }

public:
    F func_;
};
//任务封装打包器
class TaskWrapper
{
public:
    TaskWrapper() = default;
    template<typename F>
    TaskWrapper(F &&f)
    {
        using standType =  TaskImpl<F>;
        //typedef     TaskImpl<F>  standType;
        pTask_ = std::make_unique<standType>(std::forward<F>(f));
    }
    ~TaskWrapper() = default;
public:
    TaskWrapper(TaskWrapper&& other)noexcept :pTask_(std::move(other.pTask_)) {}
    TaskWrapper& operator = (TaskWrapper&& rhs)noexcept
    {
        pTask_ = std::move(rhs.pTask_);
        return *this;
    }
    TaskWrapper(const TaskWrapper&) = delete;
    TaskWrapper& operator=(const TaskWrapper&) = delete;
public:
    void operator()()const 
    {
        pTask_->operator()();
        //pTask_->DoWork();
    }
public:
    std::unique_ptr<BaseTask> pTask_;
};
//线程任务管理类
class ThreadWork
{
public:
    ThreadWork() = default;
    ~ThreadWork() = default;
public:
public:
    std::queue<std::unique_ptr<BaseTask>> qTask_;
    std::queue<TaskWrapper> qW_;
    //std::queue<std::unique_ptr<TaskWrapper>> qpW_;
};

void TestTask()
{
    //分三种情况
    //普通函数
    TaskWrapper tw1{ WorkTask };
    tw1();

    //仿函数
    TaskWrapper tw2{ WorkTaskFunc{} };
    tw2();

    //Lambda表达式
    TaskWrapper tw3{ tasklambda };
    tw3();

    ThreadWork tWork;
    tWork.qW_.emplace(std::move(tw1));
    tWork.qW_.emplace(std::move(tw2));
    tWork.qW_.emplace(std::move(tw3));

    auto t1 = std::move(tWork.qW_.front());
    tWork.qW_.pop();

    auto t2 = std::move(tWork.qW_.front());
    tWork.qW_.pop();

    auto t3 = std::move(tWork.qW_.front());
    tWork.qW_.pop();

    t1();
    t2();
    t3();

}
int main()
{
    TestTask();
    system("pause");

    return 0;
}

这个至简的框架Demo,只是一个基本的描述,到一个真正的工程模块,还有遥远的距离,但是从思想上掌握了这种开发的方式,就能够更好的指导着代码的设计和开发,这才是学习别人借鉴别人的经验的意义。学以致用,这才是最重要的。
在上面如果队列使用智能指针,或者在打包类中等使用std::shared_ptr,就可以不进行移动复制等函数的控制,但那样做的话,前者应用起来不是特别友好,不符合开发者的习惯;后者在多线程中就可能产生对象的控制的同步复杂性。

三、总结

从很早就从网上书上看到很多牛人写这个任务队列,自己也曾经反复尝试写过类似的多线程队列操作,都非常不满意。即使到现在觉得这个东西仍然有很大的完善和修改空间,有机会还是要在实际场景中对此类型的需求进行一次整体的重构。希望还能找到这个机会。


http://www.niftyadmin.cn/n/23121.html

相关文章

算法设计与分析复习03:动态规划算法

算法设计与分析复习03&#xff1a;动态规划算法 文章目录算法设计与分析复习03&#xff1a;动态规划算法复习重点动态规划算法斐波那契数列及其应用矩阵链乘法凸多边形剖分矩阵链乘法凸多边形剖分最长公共子序列最大子段和&#xff08;字数组&#xff09;0-1背包编辑距离钢条切…

【Linux】Linux进程的理解

如果不改变自己&#xff0c;就别把跨年搞的和分水岭一样&#xff0c;记住你今年是什么吊样&#xff0c;明年就还会是什么吊样&#xff01;&#xff01;&#xff01; 文章目录一、冯诺依曼体系结构&#xff08;硬件&#xff09;二、操作系统&#xff08;软件&#xff09;1.操作…

MySQL添加用户及用户权限管理

目录 1、用户 <1> 用户信息 <2> 创建用户 <3> 删除用户 <4> 修改用户密码 2、用户权限管理 <1> 查看用户权限 <2> 给用户授权 <3> 回收权限 1、用户 <1> 用户信息 MySQL中的用户&#xff0c;都存储在系统数据库mysq…

Kurganov-Tadmor二阶中心格式:理论介绍

简介 CFD的核心问题是求解双曲偏微分方程 ∂∂tu(x,t)∂∂xf(u(x,t))0\frac{\partial}{\partial t} u(x, t)\frac{\partial}{\partial x} f(u(x, t))0 ∂t∂​u(x,t)∂x∂​f(u(x,t))0在CFD中&#xff0c;双曲偏微分方程一般使用Godunov型迎风格式求解。但是这种迎风格式往往实…

【练习】Day01(未完成版)

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、选择二、编程1. 子集2. 组合答案1. 选择2. 编程普通小孩也要热爱生活&#xff01; 一、选择 导出类/ 派生类调用基类的构造器必须用到的关键字&#xff08; &#xff09; A: this B: final C: super D: static …

ext4 extent详解1之示意图演示

本文将从内核源码、实例演示等角度详细ext4 extent B树的前世今生&#xff0c;希望看过本文的读者从理解ext4 extent的工作原理。内核版本3.10.96&#xff0c;详细内核详细源码注释见GitHub - dongzhiyan-stack/kernel-code-comment: 3.10.96 内核源代码注释。 1 ext4 extent由…

【数据结构】时间与空间复杂度

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;《初识C语言》 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言一、算法效率1.1 如何衡量一个算…

k8s 1.25学习6 - ConfigMap Secret

ConfigMap存放配置信息 基于目录创建ConfigMap mkdir configmap cd configmap mkdir conf cd conf vi game.conf color.goodpurple color.badyellowvi game2.conf enemy.typesaliens,monsters player.maximum-lives5 cd ..#基于目录创建ConfigMap&#xff0c;cm名称中不能有…