目录
[21.1 空 基 类 优 化 ( The Empty Base Class Optimization , EBCO)](#21.1 空 基 类 优 化 ( The Empty Base Class Optimization , EBCO))
[21.1.1 布局原则](#21.1.1 布局原则)
[21.1.2 将数据成员实现为基类](#21.1.2 将数据成员实现为基类)
[21.2 The Curiously Recurring Template Pattern (CRTP)](#21.2 The Curiously Recurring Template Pattern (CRTP))
[21.2.1 The Barton-Nackman Trick](#21.2.1 The Barton-Nackman Trick)
[21.2.2 运算符的实现(Operator Implementations)](#21.2.2 运算符的实现(Operator Implementations))
[21.2.3 Facades](#21.2.3 Facades)
[迭代器的适配器(Iterator Adapters)](#迭代器的适配器(Iterator Adapters))
[21.3 Mixins(混合?)](#21.3 Mixins(混合?))
[21.3.1 Curious Mixins](#21.3.1 Curious Mixins)
[21.3.2 Parameterized Virtuality(虚拟性的参数化)](#21.3.2 Parameterized Virtuality(虚拟性的参数化))
[21.4 Named Template Arguments(命名的模板参数)](#21.4 Named Template Arguments(命名的模板参数))
21.1 空 基 类 优 化 ( The Empty Base Class Optimization , EBCO)
C++中的类经常是"空"的,也就是说它们的内部表征在运行期间不占用内存。典型的情况 是那些只包含类型成员,非虚成员函数,以及静态数据成员的类。而非静态数据成员,虚函 数,以及虚基类,在运行期间则是需要占用内存的。
21.1.1 布局原则
虽然在 C++中没有内存占用为零的类型,但是 C++标准却指出,在空 class 被用作基类的时候, 如果不给它分配内存并不会导致其被存储到与其它同类型对象或者子对象相同的地址上,那 么就可以不给它分配内存。
下面通过一些例子来看看实际应用中空基类优化(empty class optimization,EBCO)的意义。
cpp
#include <iostream>
class Empty {
using Int = int;// type alias members don't make a class nonempty
};
class EmptyToo : public Empty {
};
class EmptyThree : public EmptyToo {
};
int main()
{
std::cout << "sizeof(Empty): " << sizeof(Empty) << '\n';
std::cout << "sizeof(EmptyToo): " << sizeof(EmptyToo) << '\n';
std::cout << "sizeof(EmptyThree): " << sizeof(EmptyThree) << '\n';
}
如果你所使用的编译器实现了 EBCO 的话,它打印出来的三个 class 的大小将是相同的,但 是它们的结果也都不会是零。
考虑一种 EBCO 不适用的情况:
cpp
#include <iostream>!
class Empty {
using Int = int; // type alias members don't make a class nonempty
};
class EmptyToo : public Empty {
};
class NonEmpty : public Empty, public EmptyToo {
};
int main(){
std::cout <<"sizeof(Empty): " << sizeof(Empty) <<'\n';
std::cout <<"sizeof(EmptyToo): " << sizeof(EmptyToo) <<'\n';
std::cout <<"sizeof(NonEmpty): " << sizeof(NonEmpty) <<'\n';
}
EBCO 之所以会有这一限制,是因为我们希望能够通过比较两个指针来确定它们所指向的是 不是同一个对象。由于指针在程序中几乎总是被表示为单纯的地址,因此就需要我们来确保 两个不同的地址(比如指针的值)指向的总是两个不同的对象。
这一限制可能看上去并不是那么重要。但是,在实践中却经常遇到,因为有些类会倾向于从 一组空的、定义了某些基本类型别名的类做继承。当两个这一类 class 的子对象被用于同一 个完整类型中的时候,这一优化方案会被禁止。
21.1.2 将数据成员实现为基类
由于模板参数经常会被空 class 类型替换,因此在模板上下文中这一问题要更有意思一些, 但是通常我们不能依赖这一规则。如果我们对类型参数一无所知,就不能很容易的使用 EBCO。考虑下面的例子:
cpp
template<typename T1, typename T2>
class MyClass {
private:
T1 a;
T2 b; ...
};
其中的一个或者两个模板参数完全有可能被空 class 类型替换。如果真是这样,那么 MyClass这一表达方式可能不是最优的选择,它可能会为每一个 MyClass的实 例都浪费一个字的内存。
这一内存浪费可以通过把模板参数作为基类使用来避免:
cpp
template<typename T1, typename T2>
class MyClass : private T1, private T2 {
};
但是这一直接的替代方案也有其自身的缺点:
当 T1 或者 T2 被一个非 class 类型或者 union 类型替换的时候,该方法不再适用。
在两个模板参数被同一种类型替换的时候,该方法不再适用(虽然这一问题简单地通过 增加一层额外的继承来解决)。
用来替换 T1 或者 T2 的类型可能是 final 的,此时尝试从其派生出新的类会触发错误。
即使这些问题能够很好的解决,也还有一个严重的问题存在:给一个 class 添加一个基类, 可能会从根本上改变该 class 的接口。
正如在本章接下来的内容中将要看到的,从 一个模板参数做继承,会影响到一个成员函数是否可以是 virtual 的。很显然,EBCO 的这一 适用方式会带来各种各样的问题。
当已知模板参数只会被 class 类型替换,以及需要支持另一个模板参数的时候,可以使用另 一种更实际的方法。其主要思想是通过使用 EBCO 将可能为空的类型参数与别的参数"合并"。
比如,相比于这样:
cpp
template<typename CustomClass>
class Optimizable {
private:
CustomClass info; // might be empty
void* storage; ...
};
一个模板开发者会使用如下方式:
cpp
template<typename CustomClass>
class Optimizable {
private:
BaseMemberPair<CustomClass, void*> info_and_storage; ...
};
虽然还没有看到 BaseMemberPari 的具体实现方式,但是可以肯定它的引入会使 Optimizable 的实现变得更复杂。但是很多的模板开发者都反应,相比于复杂度的增加,它带来的性能提 升是值得的。我们会在第 25.5.1 节对这一内容做进一步讨论。
BaseMemberPair 的实现可以非常简洁:
cpp
#ifndef BASE_MEMBER_PAIR_HPP
#define BASE_MEMBER_PAIR_HPP
template<typename Base, typename Member>
class BaseMemberPair : private Base {
private:
Member mem;
public:// constructor
BaseMemberPair (Base const & b, Member const & m)
: Base(b), mem(m) {
}
// access base class data via first()
Base const& base() const {
return static_cast<Base const&>(*this);
}
Base& base() {
return static_cast<Base&>(*this);
}
// access member data via second()
Member const& member() const {
return this->mem;
}
Member& member() {
return this->mem;
}
};
#endif // BASE_MEMBER_PAIR_HPP
相应的实现需要使用 base()和 member()成员函数来获取被封装的(或者被执行了内存优化 的)数据成员。
21.2 The Curiously Recurring Template Pattern (CRTP)
奇异递归模板模式
另一种模式是 CRTP。这一个有着奇怪名称的模式指的是将派生类作为模板参数传递给其某 个基类的一类技术。该模式的一种最简单的 C++实现方式如下:
cpp
template<typename Derived>
class CuriousBase { ...
};
class Curious : public CuriousBase<Curious> { ...
};
一个 CRTP 的简单应用是将其用于追踪从一个 class 类型实例化出了多少对象。这一功能也可 以通过在构造函数中递增一个 static 数据成员、并在析构函数中递减该数据成员来实现。
但 是给不同的 class 都提供相同的代码是一件很无聊的事情,而通过一个基类(非 CRTP)实现 这一功能又会将不同派生类实例的数目混杂在一起。
事实上,可以实现下面这一模板:
cpp
#include <cstddef>
template<typename CountedType>
class ObjectCounter {
private:
inline static std::size_t count = 0; // number of existing objects
protected:
// default constructor
ObjectCounter() {
++count;
}
// copy constructor
ObjectCounter (ObjectCounter<CountedType> const&) {
++count;
}
// move constructor
ObjectCounter (ObjectCounter<CountedType> &&) {
++count;
}
// destructor
~ObjectCounter() {
--count;
}
public:
// return number of existing objects:
static std::size_t live() {
return count;
}
};
当我们想要统计某一个 class 的对象(未被销毁)数目时,只需要让其派生自 ObjectCounter 即可。比如,可以按照下面的方式统计 MyString 的对象数目:
cpp
#include "objectcounter.hpp"
#include <iostream>
template<typename CharT>
class MyString : public ObjectCounter<MyString<CharT>> { ...
};
int main()
{
MyString<char> s1, s2;
MyString<wchar_t> ws;
std::cout << "num of MyString<char>: "
<< MyString<char>::live() << '\n';
std::cout << "num of MyString<wchar_t>: "
<< ws.live() << '\n';
}
21.2.1 The Barton-Nackman Trick
在 modern C++中,相比于直接定义一个函数模板,在类模板中定义一个 friend 函数的好处 是:友元函数可以访问该类模板的 private 成员以及 protected 成员,并且无需再次申明该类模板的所有模板参数。
但是,在与 Curiously Recurring Template Pattern(CRTP)结合之后,friend 函数定义可以变的更有用一些,就如在下面一节中节介绍的那样。
21.2.2 运算符的实现(Operator Implementations)
当需要将一部分行为分解放置到基类中,同时需要保存派生类的标识时,CRTP 会很有用。 结合 Barton-Nackman,CRTP 可以基于一些简单的运算符为大量的运算符提供统一的定义。 这些特性使得 CRTP 和 Barton-Nackman 技术被 C++模板库的开发者所钟爱
21.2.3 Facades
将 CRTP 和 Barton-Nackman 技术用于定义某些运算符是一种很简便的方式。我们可以更近一 步,这样 CRTP 基类就可以通过由 CRTP 派生类暴露出来的相对较少(但是会更容易实现) 的接口,来定义大部分甚至是全部 public 接口。这一被称为 facade 模式的技术,在定义需 要支持一些已有接口的新类型(数值类型,迭代器,容器等)时非常有用。
为了展示 facade 模式,我们为迭代器实现了一个 facade,这样可以大大简化一个符合标准 库要求的迭代器的编写。一个迭代器类型(尤其是 random access iterator)所需要支持的接 口是非常多的。下面的一个基础版的 IteratorFacade 模板展示了对迭代器接口的要求:
cpp
template<typename Derived, typename Value, typename Category,
typename Reference = Value&, typename Distance = std::ptrdiff_t>
class IteratorFacade
{
public:
using value_type = typename std::remove_const<Value>::type;
using reference = Reference;
using pointer = Value*;
using difference_type = Distance;
using iterator_category = Category;
// input iterator interface:
reference operator *() const { ... }
pointer operator ->() const { ... }
Derived& operator ++() { ... }
Derived operator ++(int) { ... }
friend bool operator== (IteratorFacade const& lhs,
IteratorFacade const& rhs) { ... } ...
// bidirectional iterator interface:
Derived& operator --() { ... }
Derived operator --(int) { ... }
// random access iterator interface:
reference operator [](difference_type n) const { ... }
Derived& operator +=(difference_type n) { ... } ...
friend difference_type operator -(IteratorFacade const& lhs,
IteratorFacade const& rhs) { ...
}
friend bool operator <(IteratorFacade const& lhs,
IteratorFacade const& rhs) { ... } ...
};
为了简洁,上面代码中已经省略了一部分声明,但是即使只是给每一个新的迭代器实现上述 代码中列出的接口,也是一件很繁杂的事情。幸运的是,可以从这些接口中提炼出一些核心 的运算符
对于所有的迭代器,都有如下运算符:
解引用(dereference):访问由迭代器指向的值(通常是通过 operator *和->)。
递增(increment):移动迭代器以让其指向序列中的下一个元素。
相等(equals):判断两个迭代器指向的是不是序列中的同一个元素。
对于双向迭代器,还有:
递减(decrement):移动迭代器以让其指向列表中的前一个元素。
对于随机访问迭代器,还有:
前进(advance):将迭代器向前或者向后移动 n 步。
测距(measureDistance):测量一个序列中两个迭代器之间的距离。
Facade 的作用是给一个只实现了核心运算符(core operations)的类型提供完整的迭代器接 口。IteratorFacade 的实现就涉及到到将迭代器语法映射到最少量的接口上。
在下面的例子 中,我们通过成员函数 asDerived()访问 CRTP 派生类:
cpp
Derived& asDerived() {
return *static_cast<Derived*>(this);
}
Derived const& asDerived() const {
return *static_cast<Derived const*>(this);
}
有了以上定义,facade 中大部分功能的实现就变得很直接了。下面只展示一部分的迭代器接 口,其余的实现都很类似:
cpp
reference operator*() const {
return asDerived().dereference();
}
Derived& operator++() {
asDerived().increment();
return asDerived();
}
Derived operator++(int) {
Derived result(asDerived());
asDerived().increment();
return result;
}
friend bool operator== (IteratorFacade const& lhs, IteratorFacade const& rhs) {
return lhs.asDerived().equals(rhs.asDerived());
}
定义一个链表的迭代器
结合以上 IteratorFacade 的定义,可以容易地定义一个指向简单链表的迭代器。比如,链表 中节点的定义如下:
cpp
template<typename T>
class ListNode
{
public:
T value;
ListNode<T>* next = nullptr;
~ListNode() { delete next; }
};
通过使用 IteratorFacade,可以以一种很直接的方式定义指向这样一个链表的迭代器:
cpp
template<typename T>
class ListNodeIterator
: public IteratorFacade<ListNodeIterator<T>, T,
std::forward_iterator_tag>
{
ListNode<T>* current = nullptr;
public:
T& dereference() const {
return current->value;
}
void increment() {
current = current->next;
}
bool equals(ListNodeIterator const& other) const {
return current == other.current;
}
ListNodeIterator(ListNode<T>* current = nullptr) :
current(current) { }
};
通过使用 IteratorFacade,可以以一种很直接的方式定义指向这样一个链表的迭代器:
cpp
template<typename T>
class ListNodeIterator
: public IteratorFacade<ListNodeIterator<T>, T,
std::forward_iterator_tag>
{
ListNode<T>* current = nullptr;
public:
T& dereference() const {
return current->value;
}
void increment() {
current = current->next;
}
bool equals(ListNodeIterator const& other) const {
return current == other.current;
}
ListNodeIterator(ListNode<T>* current = nullptr) :
current(current) { }
};
隐藏接口
上述 ListNodeIterator 实现的一个缺点是,需要将 dereference(),advance()和 equals()运算符 暴露成 public 接口。
为了避免这一缺点,可以重写 IteratorFacade:通过一个单独的访问类 (access class),来执行其所有作用于 CRTP 派生类的运算符操作。
我们称这个访问类为 IteratorFacadeAccess:
cpp
// 'friend' this class to allow IteratorFacade access to core iterator operations:
class IteratorFacadeAccess
{
// only IteratorFacade can use these definitions
template<typename Derived, typename Value, typename Category,
typename Reference, typename Distance>
friend class IteratorFacade;
// required of all iterators:
template<typename Reference, typename Iterator>
static Reference dereference(Iterator const& i) {
return i.dereference();
}...
// required of bidirectional iterators:
template<typename Iterator>
static void decrement(Iterator& i) {
return i.decrement();
}
// required of random-access iterators:
template<typename Iterator, typename Distance>
static void advance(Iterator& i, Distance n) {
return i.advance(n);
}...
};
迭代器的适配器(Iterator Adapters)
迭代器适配器(Iterator Adapters)是一种设计模式,用于通过改变现有迭代器的行为或功能,以创建新的迭代器。它们可以在现有迭代器的基础上提供更高级的迭代操作或改变迭代器的行为方式,以满足特定的需求。
21.3 Mixins(混合?)
考虑一个包含了一组点的简单 Polygon 类:
cpp
class Point
{
public:
double x, y;
Point() : x(0.0), y(0.0) { }
Point(double x, double y) : x(x), y(y) { }
};
class Polygon
{
private:
std::vector<Point> points;
public: ... //public operations
};
如果可以扩展与每个 Point 相关联的一组信息的话(比如包含特定应用中每个点的颜色,或 者给每个点加个标签),那么 Polygon 类将变得更为实用。实现该扩展的一种方式是用点的 类型对 Polygon 进行参数化:
cpp
template<typename P>
class Polygon
{
private:
std::vector<P> points;
public: ... //public operations
};
mixin提供了另一种方法来自定义类型的行为,而无需继承它。mixin本质上颠倒了继承的正常方向,因为新的类型被作为类模板的基类"混合进"了继承层级中,而不是作为一个新的派生类创建。这种方法允许引入新的数据成员和其他操作,而不需要任何接口的重复。
一个支持了 mixins 的类模板通常会接受一组任意数量的 class,并从之进行派生:
cpp
template<typename... Mixins>
class Point : public Mixins...
{
public:
double x, y;
Point() : Mixins()..., x(0.0), y(0.0) { }
Point(double x, double y) : Mixins()..., x(x), y(y) { }
};
现在,我们就可以通过将一个包含了 label 的基类"混合进来(mix in)"来生成一个 LabledPoint:
cpp
class Label
{
public:
std::string label;
Label() : label("") { }
};
using LabeledPoint = Point<Label>;
甚至是"mix in"几个基类:
cpp
class Color
{
public:
unsigned char red = 0, green = 0, blue = 0;
};
using MyPoint = Point<Label, Color>;
有了这个基于 mixin 的 Point,就可以在不改变其接口的情况下很容易的为 Point 引入额外的 信息,因此 Polygon 的使用和维护也将变得相对简单一些。
为了访问相关数据和接口,用户 只需进行从 Point 到它们的 mixin 类型(Label 或者 Color)之间的隐式转化即可。而且,通 过提供给 Polygon 类模板的 mixins,Point 类甚至可以被完全隐藏:
cpp
template<typename... Mixins>
class Polygon
{
private:
std::vector<Point<Mixins...>> points;
public: ... //public operations
};
在模板需要进行少量定制的情况下,mixin非常有用------比如用用户指定的数据装饰内部存储的对象,使用 mixins 就不需要将内部数据类型和接口暴露出来并写进文档。
21.3.1 Curious Mixins
在和第 21.2 节介绍的 CRTP 一起使用的时候,Mixins 会变得更强大。此时每一个 mixins 都是 一个以派生类为模板参数的类模板,这样就允许对派生类做额外的客制化。一个 CRTP-mixin 版本的 Point 可以被下称下面这样:
cpp
template<template<typename>... Mixins>
class Point : public Mixins<Point>...
{
public:
double x, y;
Point() : Mixins<Point>()..., x(0.0), y(0.0) { }
Point(double x, double y) : Mixins<Point>()..., x(x), y(y) { }
};
CRTP: 派生类作为基类的模板参数
cpp
template<typename Derived>
class CuriousBase { ...
};
class Curious : public CuriousBase<Curious> { ...
};
一个支持了 mixins 的类模板通常会接受一组任意数量的 class,并从之进行派生:
cpp
template<typename... Mixins>
class Point : public Mixins...
{
public:
double x, y;
Point() : Mixins()..., x(0.0), y(0.0) { }
Point(double x, double y) : Mixins()..., x(x), y(y) { }
};
class Label
{
public:
std::string label;
Label() : label("") { }
};
class Color
{
public:
unsigned char red = 0, green = 0, blue = 0;
};
using MyPoint = Point<Label, Color>;
21.3.2 Parameterized Virtuality(虚拟性的参数化)
Minxins 还允许我们去间接的参数化派生类的其它特性,比如成员函数的虚拟性。下面的简 单例子展示了这一令人称奇的技术:
cpp
#include <iostream>
class NotVirtual {
};
class Virtual {
public:
virtual void foo() {
}
};
template<typename... Mixins>
class Base : public Mixins...
{
public:
// the virtuality of foo() depends on its declaration
// (if any) in the base classes Mixins...
void foo() {
std::cout << "Base::foo()" << '\n';
}
};
template<typename... Mixins>
class Derived : public Base<Mixins...> {
public:
void foo() {
std::cout << "Derived::foo()" << '\n';
}
};
int main()
{
Base<NotVirtual>* p1 = new Derived<NotVirtual>;
p1->foo(); // calls Base::foo()
Base<Virtual>* p2 = new Derived<Virtual>;
p2->foo(); // calls Derived::foo()
}
21.4 Named Template Arguments(命名的模板参数)
cpp
template<typename Policy1 = DefaultPolicy1,
typename Policy2 = DefaultPolicy2,
typename Policy3 = DefaultPolicy3,
typename Policy4 = DefaultPolicy4>
class BreadSlicer { ...
};
可以想象,在使用这样一个模板时通常都可以使用模板参数的默认值。但是,如果需要指定 某一个非默认参数的值的话,那么也需要指定该参数前面的所有参数的值(虽然使用的可能 是它们的默认值)。 很 显 然 , 我 们 更 倾 向 于 使 用 BreadSlicer<Policy3 = Custom>的 形 式 , 而 不 是 BreadSlicer<DefaultPolicy1,DefaultPolicy2,Policy3 = Custom>。在下面的内容在,我们开发了一种几乎 可以完全实现以上功能的技术。