隔离变化
软件设计的主要目标就是适应变化,在需求中去识别变化,从而进行抽象隔离变化,并且符合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
标签:std,bridge,const,介绍,any,擦除,template,类型,FunctionPtr From: https://blog.51cto.com/u_13137973/6236799