《C++20设计模式》学习笔记——第11章享元模式

本文探讨了在C++中如何通过享元模式解决用户名重复和字符串范围处理的问题,包括使用boost::bimaps手动实现享元和boost::flyweight简化内存管理。作者还介绍了string_view和自己实现的字符串范围接口,以及如何用享元模式节约内存空间。
摘要由CSDN通过智能技术生成


"享元"有时也称为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示例,客户端并不知道程序中实际使用的享元。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值