跟我学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,就可以不进行移动复制等函数的控制,但那样做的话,前者应用起来不是特别友好,不符合开发者的习惯;后者在多线程中就可能产生对象的控制的同步复杂性。

三、总结

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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值