"享元"有时也称为token或cookie,是一个临时组件,扮演者“智能引用”的角色。享元模式通常用于具有 大量非常相似的对象的场景,并且希望存储所有这些值的 内存开销最小。
1. 用户名问题
想像在一款大型多人在线游戏中,有许多用户拥有相同的名字。如果我们反复存储该名字,则需要花费大量内存。相反,我们可以只存储该名字一次,然后存储指向该名字的每个用户的指针,这样就可以节省空间。进一步,我们可以将姓和名分开,使用两个指针分别指向姓和名。而且,如果使用索引而不是指针,则可以进一步减少使用的字节数。
//提供两种方法,一种使用boost::bimaps手动实现一个享元;另一种直接使用boost::flyweight
//使用boost::bimaps手动实现享元
struct User
{
User(const std::string& f_n, const std::string& l_n)
: first_name_{add(f_n)}, last_name_{add(l_n)} {}
const std::string& get_first_name() const
{
return names_.left.find(first_name_)->second;
}
const std::string& get_last_name() const
{
return names_.left.find(last_name_)->second;
}
friend std::ostream& operator<<(std::ostream& os, const User& obj)
{
return os << "first name: " << obj.get_first_name()
<< " last name: " << obj.get_last_name();
}
protected:
key first_name_, last_name_;
static boost::bimaps::bimap<key, std::string> names_;
static key seed;
static key add(const std::string& s);
};
key User::add(const std::string& s)
{
auto it = names_.right.find(s);
if(it == names_.right.end())
{
//add it
names_.insert({++seed, s});
return seed;
}
return it->second;
}
//采用boost::flyweight的实现
using namespace ::boost;
using namespace ::boost::flyweights;
struct User2
{
flyweight<std::string> first_name_, last_name_;
User2(const std::string& f, const std::string& l)
: first_name_{f}, last_name_{l} {}
friend std::ostream& operator<<(std::ostream& os, const User2& obj)
{
//打印输出的时候,可以加.get(),也可以不加
return os << "first name: " <<obj.first_name_.get() << " last name: " << obj.last_name_;
}
};
2. 字符串的范围
C++中与字符串的范围相关的特性是string_view
,它是在string
类型出现很久之后才出现的;
本节中,我们将构建自己的非常简单的基于字符串范围的接口。假设在我们定义的类中已存储了一些文本格式的字符串,我们想要获取其中某一段范围的文本,并将其修改为大写字母的形式。虽然可以直接在底层的文本数据上修改,但我们希望底层的原始文本数据保持不变,而只在使用流输出运算符的时候才将选定范围的文本改写为大写的形式。
2.1 幼稚解法
一种非常愚蠢的解法是:定义一个大小与纯文本字符串长度相同的布尔数组,数组中每个元素标识文本串中的字符是否大写:
//幼稚解法
class FormattedText
{
std::string plainText_;
bool* caps;
public:
explicit FormattedText(const std::string& text) : plainText_{text}
{
caps = new bool[plainText_.length()]; //分配空间的初始值是什么呢?
memset(caps, false, plainText_.length()); //是否需要初始化设置一下内存的值为false?
}
~FormattedText()
{
delete[] caps;
}
//将指定范围的字符修改为大写形式
void capitalize(int start, int end)
{
if(start > end)
{
return;
}
for(int i=start; i<=end; ++i)
{
if(i<0) {
continue;
}
if(i>=plainText_.length()) {
break;
}
caps[i] = true;
}
}
friend std::ostream& operator<<(std::ostream& os, const FormattedText& obj)
{
std::string s;
for(int i=0; i<obj.plainText_.length(); ++i)
{
char c = obj.plainText_[i];
s += (obj.caps[i] ? toupper(c) : c);
}
return os << s;
}
};
这种方法除了浪费内存,而且很难扩展:想像一下,如果我们还想给文本加下划线或者使其变为斜体,那么将引入更多的布尔数组,这会浪费更多内存空间!
2.2享元实现
其实只使用开始标记和结束标记就可以了。再次尝试使用享元模式的解法如下:
//字符串范围的享元实现
class BetterFormattedText
{
public:
BetterFormattedText(const std::string& str) : plain_text_{str} {}
struct TextRange
{
int start_, end_;
bool capitalize_{false};
//other options here, e.g. bold, italic, etc.
//determine our range covers a particular position
bool covers(int position) const
{
return position >=start_ && position <= end_;
}
//constructor
};
private:
std::string plain_text_;
std::vector<TextRange> formatting_;
friend std::ostream& operator<<(std::ostream& os, const BetterFormattedText& obj)
{
std::string s;
for(size_t i=0; i<obj.plain_text_.length(); ++i)
{
auto c = obj.plain_text_[i];
for(const auto& rng : obj.formatting_)
{
if(rng.covers(i) && rng.capitalize_)
{
c = toupper(c);
}
}
s += c;
}
return os << s;
}
public:
TextRange& get_range(int start, int end)
{
formatting_.emplace_back(TextRange{start, end});
return *formatting_.rbegin();
}
};
此方案可以添加其他格式信息(如粗体、斜体等),还允许设置重叠的范围。当然,在每个范围内进行这样的线型搜索是低效的,但我们依然这样做,因为我们关心的是能否节约内存空间,而不是性能。
3. 总结
享元模式本质上是一种节约内存空间的计数。它的具体体现是多种多样的:
- 有时会将享元类作为API token返回,以对该享元进行修改;
- 有时候享元是隐式的,隐藏在幕后——就像User示例,客户端并不知道程序中实际使用的享元。