类型擦除介绍
  ZXL8ALkrtBPG 2023年11月02日 51 0
c++

隔离变化

       软件设计的主要目标就是适应变化,在需求中去识别变化,从而进行抽象隔离变化,并且符合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

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  gBkHYLY8jvYd   2023年11月19日   27   0   0 #includecic++
  ZXL8ALkrtBPG   2023年11月02日   67   0   0 c++
  ZXL8ALkrtBPG   2023年12月06日   24   0   0 字面量c++
  lh6O4DgR0ZQ8   2023年11月24日   18   0   0 cii++c++
  ZXL8ALkrtBPG   2023年11月12日   32   0   0 c++
  gBkHYLY8jvYd   2023年11月19日   27   0   0 十进制高精度c++
  ZXL8ALkrtBPG   2023年11月02日   52   0   0 c++
  ZXL8ALkrtBPG   2023年11月02日   53   0   0 c++
  gBkHYLY8jvYd   2023年12月11日   20   0   0 cic++最小值
  gBkHYLY8jvYd   2023年11月19日   25   0   0 测试点cic++
ZXL8ALkrtBPG