Flyweight
Flyweight, 有时也称为token或cookie, 是一种临时性的组件, 它起着智能引用smart reference的作用. 它通常是在当存在大量十分相似的对象时, 节省内存的一种手段.
11.1 User Names
假定你有一个大型多人在线游戏, 相信会有很多人叫John Smith. 因此, 如果我们用ASCII码一个个记录他们的名字, 每个用户会消耗11个字节. 相反, 我们可以存储John Smith一次, 然后为每个人存储一个指向这个名字的指针. 这样就只需要8个字节.
更进一步, 我们还可以将John Smith再一次拆分为两部分分别保存.
1 | typedef uint32_t key; |
下面是add
的实现:
1 | static key add(const string& s) |
上面的代码使用了boost::bimap
. 这是一个和标准的get-or-add的实现机制.
下面是获取实际的名字的接口:
1 | const string& get_first_name() const |
11.2 Boost.Flyweight
在前面的例子中,我们手撸了代码. 而Boost中提供了一个可用的库: boost::flyweight. 我们使用它来重写上面的例子:
1 | struct User2 |
而可以这样使用它:
1 | User2 john_doe {"John", "Doe"}; |
11.3 String Ranges
如果你调用了std::string::substring()
, 它是否返回一个新构造的string? 答案是不一定的. 如果你想对它做独立的修改, 那么答案为是. 可是如果你想对原有的字符串做修改呢? 有些编程语言( 例如, Swift, Rust) 将字串实现为使用flyweight模式的一个range以节省内存占用的同时支持对原有串的操作.
在C++中的等价物是string_view
. 另外还有一些array的变体, 它们都能够避免数据拷贝. 我们会尝试构造一个自己的string range.
假定在类中存储一些文本, 我们可以从中提取一部分文本并将其转换为大写. 当然, 我们可以直接把文本中的每个字符都改成大写. 但是假设我们还希望保留原有的文本, 只是在使用流输出操作符时大写化呢?
11.4 Naive Approach
一种很简单的做法是, 使用一个bool
的数组来记录每个字符是否要将对应的字符改为大写.
1 | class FormattedText |
现在就可以使用它了:
1 | void capitalize(int start, int end) |
然后定义stream <<
操作符:
1 | friend std::ostream& operator<<(std::ostream& os, const FormattedText& obj) |
上面的东西是可以用的:
1 | FormattedText ft("This is a brave new world"); |
当然, 这个实现很蠢. 它为每个字符都定义了一个bool的flag. 而实际上, 我们只需要start
和end
标志就足够了. 下面使用FlyWeight模式来重新实现它:
11.5 Flyweight Implementation
1 | class BetterFormattedText |
TextRange
只是存储了区域的起始位置和实际的格式化信息. 它只有一个成员函数covers()
, 用于判断给定位置的字符是否需要做特殊的格式化处理.
BetterFormattedText
在一个vector
中存储TextRange
.
1 | TextRange& get_range(int start, int end) |
这个函数做了三件事:
- 创建了一个新的
TextRange
对象 - 将它移动到
vector
中 - 返回它的引用
在这个实现中, 我们还没有检查重复的和冲突的区段–它还可能能够进一步节省内存空间.
接下来实现<<
操作符:
1 | friend std::ostream& operator<<(std::ostream& os, |
使用代码没有变化:
1 | BetterFormattedText bft("This is a brave new world"); |