隔离变化
软件设计的主要目标就是适应变化,在需求中去识别变化,从而进行抽象隔离变化,并且符合SOLID准则,良好的设计可以让程序员少加些班,好保住“猿类”们那珍惜的毛发。
有过面向对象开发经验的同学自然能想到继承,通过抽象基类实现多态,来隔离不同派生类型进行差异处理:

用户依赖于抽象基类,与具体的实现类型解耦:
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();
}对应的类图关系为:

其中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对象

相关的关键代码已在上图的中标识,应用SOO即根据对象的大小决定是否使用堆内存申请,这样对于小对象可以获得更好的性能,例如在栈上创建一个any对象时,小对象就可以直接使用栈内存;
reset any对象
即调用any的reset方法,即:
struct Circle {
    ~Circle()
    { 
        DBG_LOG();
    }   
    // ...
};
int main()
{
    std::any a = std::make_any<Circle>();
    a.reset();
}此时小对象则需要调用Circle的析构,大对象除了需要调用Circle的析构还要释放对应的堆内存。

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

在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










