垫片(Shim)
在C++中,逻辑相关的类型通常具有不兼容的接口和操作,这使得人们有时难于、甚至无法进行泛化的类型操纵。
在泛型世界里,各式各样的语法妨碍语义的例子不计其数。当我们将一个类型改变为另一个跟它语义类似的类型时,总希望编译器能够帮我们打点一切,但显然它不能,除非我们可以帮帮它。因此,我们的做法就是往缝隙里插入一些垫片(shim),从而让一些东西妥当地贴合起来。软件工程中有个经典的“万灵药”,即增加一个间接层。这正是垫片概念全部的本质。
在模板出现以前的古典主义C++里,基于虚函数表的多态机制体现出的是名字一致性:即派生类重写父类同名的方法。而模板函数的出现带来了结构一致性:对于看起来一样的东西,我们可以期望它们具有相同的行为。简单地说,结构一致性确保类型的编译器兼容,而名字一致性则取保类型的运行期兼容性。
但结构一致性的弱点在于结构上一致的代码很可能做的是语义上不一致的事情。而垫片的使用在避免这个问题方面迈出了显著的一步。垫片表现了一种约定,借助该约定,结构一致性得以扩展以包含每个垫片对应的一个明确的语义。垫片是对所谓的结构一致性的一种提炼和升华,即语义一致性。
- 特性垫片
特性垫片用于从它们为之定义的类型的实例身上抽取某些特性或状态。如:
// 举例:特性垫片
// 从各种指针类型上取出出原生指针特性。
template<typename T>
inline T* get_ptr(T* p)
{
return p;
}
template<typename TL
inline T* get_ptr(std::auto_ptr<T>& p)
{
return p.get();
}
template<typename T>
inline T const * get_ptr(std::auto_ptr<T> const & p)
{
return p.get();
}
template<typename T>
inline T* get_ptr(comstl::interface_ptr<T>& p)
{
return p.get_interface_ptr();
}
- 逻辑垫片
逻辑垫片是特性垫片的一个精化,它们用于汇报实例的状态。
// 举例:逻辑垫片
// 泛化对任何容器的状态的访问。
template<typename T>
bool is_empty(T const & c)
{
return c.empty();
}
bool is_empty(CString const & s)
{
return s.IsEmpty();
}
bool is_empty(comstl::interface_ptr const & p)
{
return NULL == p.get_interface_ptr();
}
- 控制垫片
控制垫片用于操纵它们所服务的类型的实例。
如:make_empty() 或 dump_contents()等。
- 转换垫片
转换垫片将一组互相兼容的类型的实例转换至同一个目标类型。
转换垫片的返回值可能由中间临时对象提供,对于这种垫片,其返回值只能在包含该垫片的表达式当中被使用。
- 访问垫片(复合式垫片)
访问垫片是特性垫片和转换垫片的复合体,被用来访问它们为之定义的类型的实例值。
// 举例:访问垫片
// 将构造函数的参数转型为C const*,然后转递给init()方法,因此该类可以被用在任何字符串类型上。
template<typename C>
class X
{
public:
explicit X(C const * p)
{
init(c_str_ptr(p));
}
template<typename S>
explicit X(S const & s)
{
init(c_str_ptr(s));
}
...
private:
void init(C const * p);
...
};
inline char const * c_str_ptr(char const * p)
{
return p;
}
inline wchar_t const * c_str_ptr(wchar_t const * s)
{
return s;
}
template<typename T>
inline T const * c_str_ptr(std::basic_string<T> const & s)
{
return s.c_str();
}
template<typename T>
inline T const * c_str_ptr(stlsoft::basic_frame_string<T> const & s)
{
return s.c_str();
}
// 应用场景
//
char const * s1 = ...
std::basic_string<char> s2 = ...
std::basic_string<wchar_t> s3 = ...
X<char> o1(s1);
X<char> o2(s2);
X<wchar_t> o3(s3;
饰面(Veneer)
饰面用于将类型或功能一种精细的方式覆盖于现行类型之上,饰面通常于将“最终接触面”覆盖在一个现有的、实质性的类型之上。饰面也可以用于将特定的行为绑定到一个简单的类型上。
饰面是一种具有如下特征的类模板:
- 它继承自它的主参数化类型,而且通常是公有继承。
- 它适应并遵循它的主参数化类型的多态性质,这意味着饰面不能定义它自己的任何虚函数,尽管它可以重写它的主参数化类型中定义的那些虚函数。
- 它不能定义属于自己的任何非静态成员变量。
第2、3点意味着饰面不能改变它的主参数化类型的内存占用多少,这是通过EDO(Empty Derived Optimization,空派生类优化)实现的。
螺栓(Bolt)
饰面和螺栓之间的区别并不明显,但它们是为不同的意图而诞生的。饰面是用于“润饰”现存类型的,而螺栓则是用于显著改变或完善类型的行为特征的。
螺栓是具有如下特征的类模板:
- 它们继承(通常是公有继承)自它们的主参数化类型。
- 它们和它们的主参数化类型的多态性质相适应。但除了重写虚函数外,还可以定义自己的虚函数。
- 由于它们可以定义自己的成员变量、虚函数或从另外的非空类继承,所以它们可能会增添额外的内存占用。
拟编译期多态:逆反式螺栓
template<typename T>
struct Base
{
void Do()
{
static_cast<T*>(this)->Do();
}
};
struct Derived : public Base<Derived>
{
void Do();
};
template<typename T>
void f(Base<T>* p)
{
p->Do();
}
通过上例,可以看到,我们通过编译期多态,替代了运行期多态(虚函数),而实现了设计模式之模板方法,避免了运行期间接调用所带来的开销。