首页 > 其他分享 >类型擦除介绍

类型擦除介绍

时间:2023-04-29 15:32:06浏览次数:35  
标签:std bridge const 介绍 any 擦除 template 类型 FunctionPtr

隔离变化

       软件设计的主要目标就是适应变化,在需求中去识别变化,从而进行抽象隔离变化,并且符合SOLID准则,良好的设计可以让程序员少加些班,好保住“猿类”们那珍惜的毛发。

      有过面向对象开发经验的同学自然能想到继承,通过抽象基类实现多态,来隔离不同派生类型进行差异处理:

类型擦除介绍_C++

用户依赖于抽象基类,与具体的实现类型解耦:

struct User {
    User(std::unique_ptr<Shape> shape) : m_shape (std::move(shape)) {}
    void Process()
    {
        m_shape->Draw();
    }
 private:
    std::unique_ptr<Shape> m_shape;
};  

int main()
{   
    User user(std::make_unique<Circle>());
    user->Process();
}

       User需要持有基类的指针并且需要负责其生命周期的管理;同时可以发现一个问题,当User需要进行拷贝时无法对Shape进行深拷贝,因为不知道其具体派生类型。

      而在C语言中,void*指针此时便承担了隔离依赖的作用,对此有过经验的同学都应该或多或少体会过被void *支配的恐惧,强转类型而导致内存越界、内存被异常改写等问题,曾经我们也为它付出过大好青春。

类型擦除

TypeErasure 提供了一种设计方法来优化上述的问题:

struct Shape {
    struct ShapeConcept {
        virtual ~ShapeConcept() {}
        virtual void Draw() const = 0;

        // The Prototype Design Pattern
        virtual std::unique_ptr<ShapeConcept> Clone() const = 0;
    };

    template <typename T>
    struct ShapeModel : ShapeConcept {

        ShapeModel(const T& value)
            : m_object{value} {
        }

        void Draw() const override
        {
            m_object.Draw();
        }

        // The Prototype Design Pattern
        std::unique_ptr<ShapeConcept> Clone() const override
        {
            return std::make_unique<ShapeModel>(*this);
        }

    private:
        T m_object;
    };

    // A constructor template to create a bridge. 
    template <typename T> requires requires(T t){ t.Draw(); }
    Shape(const T& x)
      : m_pimpl{std::make_unique<ShapeModel<T>>(x)} { }

    Shape(const Shape& s)
      : m_pimpl{s.m_pimpl->Clone()} { }

    void Draw()
    {
        m_pimpl->Draw();
    }
private:
    // The Bridge Design Pattern
    std::unique_ptr<ShapeConcept> m_pimpl;
};
struct Circle {
    void Draw() const { DBG_LOG(); }
};

struct User {
    User(const Shape &shape) : m_shape (std::move(shape)) {}
    void Process()
    {
        m_shape.Draw();
    }
 private:
    Shape m_shape;
};  

int main()
{
    User user(Circle{});
    user.Process();
}

对应的类图关系为:

类型擦除介绍_C++_02

其中Shape应用了桥接模式+原型模式,通过构造函数模板建立桥接,Shape擦除了类型之间的差异,用户同样不用依赖于具体类型;用户使用时更加简洁,减少了make_unique等调用,并且不需要对Circle和Square进行侵入式添加继承。

      同时这里还存在问题:创建Shape时需要动态申请内存,频繁的堆内存申请与释放会导致性能恶化及内存碎片化,这里可以应用SOO(小对象优化)进行改进,在下一节std::any的实现中同样使用了此优化手段。

STL源码分析

std::function

std::function可以接受各种类型的调用:函数指针、lambda、可调用对象等【1】,对具体调用对象类型进行了擦除。

template<typename F>
void Command(F f, int arg)
{
    // ...
    f(arg);
}

Command函数本意为通过模板参数F接受不同的可调用对象类型包括lambda等,但当Command函数实现代码量大的情况下将其改造成上述函数模板,对不同类型进行模板实例化时则造成最终的二进制代码膨胀;如果F修改为函数指针则无法接受lambda表达式,存在局限性;

此时使用std::function便可以解决上述问题;参考【2】中的简化实现代码进行说明:

template<typename R,typename... Args>
class FunctorBridge {
public:
    virtual ~FunctorBridge(){}
    virtual FunctorBridge* clone() const=0;
    virtual R invoke(Args... args) const=0;
};

template<typename Functor,typename R,typename... Args>
class SpecificFunctorBridge : public FunctorBridge<R,Args...> {
    Functor functor;
public:
    template<typename FunctorFwd>
    SpecificFunctorBridge(FunctorFwd&& functor):functor(std::forward<FunctorFwd>(functor))
    {}
    virtual SpecificFunctorBridge* clone() const override
    {
        return new SpecificFunctorBridge(functor);
    }
    virtual R invoke(Args... args) const override
    {
        return functor(std::forward<Args>(args)...);
    }
};

template<typename Signature>
class FunctionPtr;

template<typename R,typename... Args>
class FunctionPtr<R(Args...)>
{
private:
    FunctorBridge<R,Args...>* bridge;
public:
    FunctionPtr():bridge(nullptr) {}

    FunctionPtr(FunctionPtr &other)
        :FunctionPtr(static_cast<FunctionPtr const&>(other)) { }

    FunctionPtr(FunctionPtr&& other):bridge(other.bridge)
    {
        other.bridge=nullptr;
    }

    template<typename F>
    FunctionPtr(F&& f) : bridge(nullptr)
    {
        using Functor=std::decay_t<F>;
        using Bridge = SpecificFunctorBridge<Functor,R,Args...>;
        bridge = new Bridge(std::forward<F>(f));
    }

    FunctionPtr& operator=(FunctionPtr const& other)
    {
        FunctionPtr tmp(other);
        swap(*this,tmp);
        return *this;
    }

    FunctionPtr& operator=(FunctionPtr&& other)
    {
        delete bridge;
        bridge=other.bridge;
        other.bridge=nullptr;
        return *this;
    }

    template<typename F>
    FunctionPtr& operator=(F&& f)
    {
        FunctionPtr tmp(std::forward<F>(f));
        swap(*this,tmp);
        return *this;
    }

    ~FunctionPtr()
    {
        delete bridge;
    }

    friend void swap(FunctionPtr& fp1,FunctionPtr& fp2)
    {
        std::swap(fp1.bridge, fp2.bridge);
    }

    explicit operator bool() const
    {
        return bridge==nullptr;
    }

    R operator()(Args... args) const
    {
        return bridge->invoke(std::forward<Args>(args)...);
    }
};

可以看到核心实现与第二节中说明的基本一致,不在赘述。

std::any

std::any为C++17引入的特性,any可以接受任意类型,擦除了具体类型;同时当使用any_cast<T>获取出实际类型时,如果T不是原来的类型,则会抛出异常。

同样我们可以使用上一节中介绍的实现来完成,但标准库中使用了SOO所以实现有所差别:

创建any对象

类型擦除介绍_C++_03

相关的关键代码已在上图的中标识,应用SOO即根据对象的大小决定是否使用堆内存申请,这样对于小对象可以获得更好的性能,例如在栈上创建一个any对象时,小对象就可以直接使用栈内存;

reset any对象

即调用any的reset方法,即:

struct Circle {
    ~Circle()
    { 
        DBG_LOG();
    }   
    // ...
};

int main()
{
    std::any a = std::make_any<Circle>();
    a.reset();
}

此时小对象则需要调用Circle的析构,大对象除了需要调用Circle的析构还要释放对应的堆内存。

类型擦除介绍_C++_04

通过函数指针隔离了具体类型的申请与销毁;

any_cast<T>

类型擦除介绍_C++_05

      在any对象创建代码中,Storage中TypeData存储了对应类型的typeid,此处any_cast会进行类型校验,校验失败则会抛出异常。

参考资料

【1】https://en.cppreference.com/w/cpp/utility/functional/function

【2】C++ Templates 2rd

【3】 Breaking Dependencies: Type Erasure - A Design Analysis

标签:std,bridge,const,介绍,any,擦除,template,类型,FunctionPtr
From: https://blog.51cto.com/u_13137973/6236799

相关文章

  • 我刚才用了dynamic_cast 你给我普及一下C++ 中这几种类型转换吧
    我刚才用了dynamic_cast你给我普及一下C++中这几种类型转换吧在C++中,有几种类型转换的方式,包括:隐式转换在一些情况下,编译器会自动进行类型转换。比如将整型变量赋值给浮点型变量,编译器就会自动将整型变量转换为浮点型变量。但是在大多数情况下,使用隐式转换可能会引起一些问......
  • 关于AWS-ElastiCache的Reserved nodes预留节点支付类型-费用说明
    关于AWS-ElastiCache的Reservednodes的购买(类似于EC2的RI),可以节省成本引擎,可以选择Redis或者Memcached,期限一般大多都支持1年或者3年的对于Offeringtype-产品类型,这里分类比其他产品要复杂一点、,分为【标准预留节点产品】与【旧式预留节点产品】这个还与节点类型有关系......
  • 关于AWS-ElastiCache的总体介绍
    AmazonElastiCachemakesiteasytosetup,manage,andscaledistributedin-memorycacheenvironmentsintheAWSCloud.Itprovidesahighperformance,resizable,andcost-effectivein-memorycache,whileremovingcomplexityassociatedwithdeployingand......
  • Python数据类型详解
    Python最基本的内置数据类型包括:布尔型(表示真假的类型,仅包含True和False两种取值);整型(整数,例如42、10000000);浮点型(小数,例如3.14159,或用科学计数法表示的数字,例如1.0e8,它表示1乘以10的8次方,也可写作10000000.0);字符串型(字符组成的序列);一、变量Python里所有数据——布尔值、整数、浮点......
  • 自动化测试工具自动化工具Pyautogui和Pywinauto详细介绍和使用
    自动化测试工具介绍和使用PC端应用程序自动化测试——pywinauto、pywin32、pyautogui详解Python中pyautogui库的最全使用方法自动化测试工具自动化工具Pyautogui和Pywinauto详细介绍和使用   1、自动化测试工具介绍和使用一.Pywinauto库的介绍二、pyautogui库的......
  • sklearn中的KFold简单介绍
    这一部分主要讲解关于什么是K-foldCV(K折交叉验证),简单的使用一些案例进行分析,然后使用sklearn库函数中一些简单的案例进行分析。在机器学习中,多数最主要的功能函数被封装到sklearn的库函数中,model_selection类中包含了K-foldCV的简单使用,可以直接使用这个进行调用。一.关于K-Fold......
  • rabbitMQ--类型
    1.五种消息模型1.1基本消息模型 1.2work消息模型 1.3订阅模型1.3.1Fanout,也称为广播。流程说明流程图: 在广播模式下,消息发送流程是这样的:1)可以有多个消费者2)每个消费者有自己的queue(队列)3)每个队列都要绑定到Exchange(交换机)4)生产者发送的消息,只能......
  • 【Python】【MySQL】Python将JSON数据以文本形式存放到MySQL的Text类型字段中
    1.起因在做一个自动打卡的玩意。登录会得到那个平台一系列的信息。我又不想专门修改、增加数据库字段来存放,所有打算直接将返回的JSON数据保存到一个MySQL字段中。内容肯定不能直接放,考虑下比如数据注入的问题,对吧,容易出问题,所有我是打算将JSON数据转为base64编码的格式。先写......
  • mockery v2的介绍和使用
    前言由于项目时间比较紧,我本来是没有打算写一篇文章来介绍mockery的,但是无奈网上介绍mockery的文章比数量上较少(截至2023-04-27),而且很多文章都过期了.一方面由于golang更新比较快,网上解释使用goget安装mockery的,到了go1.6以后都安装不了.另一方面mockery自身更......
  • 虚拟机与主机的4种网络访问类型
    虚拟机一直用,但选择网络时的四种模式总是搞不清楚,只知道选择bridge最好用。为了能更深入了了解,查询了些资料,总结如下  第一种NAT模式Vhost访问网络的所有数据都是由主机提供的,vhost并不真实存在于网络中,主机与网络中的任何机器都不能查看和访问到Vhost的存在。虚拟机......