【读书笔记】【Effective STL】关联容器

第 19 条:理解相等(equality)和等价(equivalence)的区别。

  • 相同的意义可能是相等,也可能是等价,该条款就是讨论相等和等价的区别。【要注意,许多函数都必须判断两个值是否相同

  • 相等的概念是基于 operator== 的。【find() 函数对相同的定义是相等

    • 对于两个对象 xy,如果表达式 x == y 返回 true,即 xy 有相等的值;但重要的是 operator== 是可以重载的,它们自身的成员未必一一相等。
  • 等价关系是以在已排序的区间中对象值的相对顺序为基础的。【setinsert() 成员函数对相同的定义是等价

    • 这句话的意思是:对于两个对象 xy,如果按照关联容器 c 的排列顺序,每个都不在另一个的前面,那么称这两个对象按照 c 的排列顺序有等价的值。

    • 例如 set 的默认比较函数是 less,在默认情况下 less 只是简单调用了 Toperator <;所以对于两个 T 对象 t1t2 来说,只要下面表达式结果为真,就说 t1t2 对于 operator < 有等价的值:

      !(w1 < w2)  &&  !(w2 < w1)
      
      // 更一般的情况是用户自定义的判别式
      	// 每个关联容器都通过key_comp成员函数使排序判别式可被外界使用
      !c.key_comp()(x, y) && !c.key_comp()(y, x)
      
  • 进一步理解相等和等价的区别,考虑一个不区分大小写的 set,代码如下:

    struct CIStringCompare : public binary_function<string, string, bool>{
        bool operator()(const string& lhs, const string& rhs) const{
            return ciStringCompare(lhs, rhs);//不区分大小写的函数对象,具体实现参考条款35
        }
    }
    set<string, CIStringCompare> ciss;// 不区分大小写的set
    // 【下面的分析也说明要多使用容器的成员函数(list::find)而不是非成员函数(find)】
    // 如果在set中插入STL和stl,
      //	只有第一个字符串会被插入,因为第二个和第一个等价;
    // 如果使用set的find成员函数查找stl,
      // 是可以查找成功的(也是因为stl和STL等价);
    // 如果使用非成员的find算法就会查找失败。
    
  • 标准关联容器是基于等价而不是相等,但是为什么关联容器要使用等价,而不是相等呢?

    • 标准关联容器总是保持排列顺序的,所以每个容器必须有一个比较函数来决定保持怎样的顺序。
    • 如果关联容器使用相等来决定两个对象是否相同的话,意味着要提供另一个比较函数来判断相等。【如果使用相等,那么就无法给两个对象确定先后顺序】
  • 最后总结一下:

    • 等价就是只使用一个比较函数确定两个值是否相同,而并非使用相等语义,这样做能避免很多麻烦。

第 20 条:为包含指针的关联容器指定比较类型。

  • 每当程序员要创建包含指针的关联容器时,一定要记住,容器将会按照指针的值进行排序。

  • 绝大多数情况下,这不会是程序员所希望的,所以肯定要创建自己的函数子类作为该容器的比较类型(comparison type),代码如下:

    struct StringPtrLess : public std::binary_function<const std::string*, const std::string*, bool> { // std::binary_function在C++11中已被废弃
    	bool operator()(const std::string* ps1, const std::string* ps2) const
    	{
    		return *ps1 < *ps2;
    	}
    };
     
    struct DereferenceLess {
    	template<typename PtrType>
    	bool operator()(PtrType pT1, PtrType pT2) const
    	{
    		return *pT1 < *pT2;
    	}
    };
     
    int test_item_20()
    {
    	//std::set ssp;
    			// 相当于std::set>, 得不到预期的结果
    	//std::set ssp;
    	std::set<std::string*, DereferenceLess> ssp; // 与std::set ssp;的行为相同
    	ssp.insert(new std::string("Anteater"));
    	ssp.insert(new std::string("Wombat"));
    	ssp.insert(new std::string("Lemur"));
    	ssp.insert(new std::string("Penguin"));
     
    	for (auto it = ssp.cbegin(); it != ssp.cend(); ++it) {
    		fprintf(stdout, "%s\n", (**it).c_str());
    	}
     
    	return 0;
    }
    
  • 如果有一个包含智能指针或迭代器的容器,那么也要考虑为它指定一个比较类型,对指针的解决方案同样也适用于那些类似指针的对象。

    • 就像 DereferenceLess 适合作为包含 T* 的关联容器的比较类型一样,对于容器中包含了指向 T 对象的迭代器或智能指针的情形,DereferenceLess 也同样可用作比较类型。

第 21 条:总是让比较函数在等值情况下返回 false。

  • 比较函数的返回值表明的是按照该函数定义的排列顺序,即一个值是否在另一个之前。

  • 相等的值从来不会有前后顺序关系,所以,对于相等的值比较函数应当始终返回 false

    • 考虑错误的情况,如果对于一个 set,不用 less 作为比较类型,而用 less_equal(即 ),就相当于在等值情况下返回 true,那么最后影响会如下所示:

      set<int, less_equal<int> > s;				// s以“<=”排序
      s.insert(10);						// 插入10A
      s.insert(10);						// 插入10B
      
      // set遍历它的内部数据结构以查找哪儿适合插入10B。
      	// 最终,它总要检查10B是否与10A相同。(即判断是否等价)
      !(10_A <= 10_B) && !(10_B <= 10_A)				// 测试10A和10B是否等价
      	// 上述表达式简化为:
      !(true) && !(true)
      	// 结果自然就是false
      	// 最后set就判定10A和10B不等价,又一次将10插入了set
      
  • 前面一个结论还是比较直观的,但还是要清楚违反这个规则造成的后果可能并不那么直白。

  • 首先利用条款 20 中的代码进行修改,代码的原意是想将其排序方式变成降序:

    struct StringPtrGreater:				// 高亮显示
    public binary_function<const string*,		// 这段代码和书中89页的改变
    const string*,					// 当心,这代码是有瑕疵的!
    bool> {
      bool operator()(const string *ps1, const string *ps2) const
      {
        return !(*ps1 < *ps2);			// 只是取反了旧的测试;
      }						// 这是不对的!
    };
    
    • 这段代码会出现问题,问题在于:
      • 取反 < 不会得到 >,它给你的是 >=
      • 所以它将对相等的值返回 true,对关联容器来说,它是一个无效的比较函数。
  • 最后,讨论最让人迷惑的一种情况,即 multi 系列容器如果没有遵守本条规则究竟会产生什么后果:

    // 假设当前有一 multiset:
    multiset<int, less_equal<int> > s;			// s仍然以“<=”排序
    s.insert(10);					// 插入10A
    s.insert(10);					// 插入10B
    
    // 问题在于后续的操作:
    equal_range(s.begin(), s.end(), 10);// 返回等于10的迭代器范围
    // 前面说过了,关联容器中的比较都是基于等价的
      // 所以这里的equal_range是取等价为10的值的范围
    
    // 检查10A和10B等价与否,用下面的表达式:
    !(10_A <= 10_B) && !(10_B <= 10_A)
    // 会变成如下结果:
    !(true) && !(true)
    // 最后结果当然就是false
    // 所以最后结果表明10A不等价于10B,
      // equal_range就会失效
    

第 22 条:切勿直接修改 set 和 multiset 的键。

  • 本条款讨论的内容很直观。

    • setmultiset 保持它们的元素有序,这些容器的正确行为依赖于它们保持有序;如果改变了某个值(例如把 10 变为 1000),新值可能不在正确的位置上,而且那将破坏容器的有序性。
    • 而对于 mapmultimap 类型的对象而言,元素的类型是 pair;所以键是不允许改变的。【但本条款重心不在 mapmultimap 键值】
  • 对于 setmultiset 类型的对象,容器中元素的类型是 T,而不是 const T;虽然没有 const 特性,但不能轻易地直接修改键值。

  • 假设一个场景如下:

    // 有一个雇员类,即set用来存储的类
    class Employee {
    public:
    	...
    	const string& name() const;			// 获取雇员名
    	void setName(const string& name);		// 设置雇员名
    	const string& getTitle() const;		// 获取雇员头衔
    	void setTitle(string& title);		// 设置雇员头衔
    	int idNumber() const;			// 获取雇员ID号
    	...
    }
    
    // 该函数类是用作比较函数,即只取雇员类的idNumber来排序
    struct IDNumberLess{
    	public binary_function<Employee, Employee, bool> {	// 参见条款40
    		bool operator()(const Employees lhs,
    				const Employee& rhs) const
    		{
    			return lhs.idNumber() < rhs.idNumber();
    		}
    };
    
    typedef set<Employee, IDNumberLess> EmpIDSet;
    EmpIDSet se;						// se是雇员的set,
    							// 按照ID号排序
    
  • 修改键值的操作如下:

    // 修改操作如下所示:
    Employee selectedID;					// 容纳被选择的雇员
    ...							// ID号的变量
    EmpIDSet::iterator i = se.find(selectedID);
    if (i != se.end()){
    	i->setTitle("Corporate Deity");			// 给雇员新头衔
    }
    		// 有些STL实现会拒绝这行
    		// 因为*i是const
    
  • 前面例子旨在说明:

    • 即使 setmultiset 的元素不是 const,实现仍然有很多方式可以阻止它们被修改。
    • 例如,实现可以让用于 set::iteratoroperator* 返回一个常数 T&。(即前面修改键值的操作结果就是如此,假设 *iconst 的,所以 STL 实现就会拒绝设置新头衔的那一行)
  • 想要解决问题,其实将其常量移除即可,代码如下:【要注意必须强制转换到引用】

    // 正确的版本:【映射到引用】
    if (i != se.end()) {// 转换掉*i的常量性
        const_cast<Employee&>(*i).setTitle("Corporate Deity");
    }
    
    // 错误的版本:【可通过编译,但是实际是错误的】
        // 版本一:
    if (i != se.end()){	// 把*i转换成一个Employee
        static_cast<Employee>(*i).setTitle("Corporate Deity");
    }
        // 等价的版本二:
    if (i != se.end()) {// 同上,但使用C方式的类型转换
        ((Employee)(*i)).setTitle("Corporate Deity");	
    }
        // 等价的版本三:(看清楚究竟不转成引用为什么会出错)
    if (i != se.end()){
        Employee tempCopy(*i);				// 把*i拷贝到tempCopy
        tempCopy.setTitle("Corporate Deity");		// 修改tempCopy,*i根本没有修改
    }
    
  • 前面已经将本条款的内容讲解了大半,总结如下:

    • setmultiset 虽然键值本身不是 const 的,但是有很多其他操作导致它的修改会被阻止;所以用强制转换可以移除 const 来进行修改。【要注意它本身不是 const,只不过经过 setmultiset 的函数才转成了 const
    • 但是,mapmultimap 的键值本身是 const 的,所以最好就不要强制转换移走常量特性。
  • 大多数的强制转换是可以避免的,在本条款的最后,作者提出了按照五个步骤去安全改变 setmultisetmapmultimap 里的元素,步骤如代码所示:

    EmpIDSet se;					// 同前,se是一个以ID号排序的雇员set
    Employee selectedID;				// 同前,selectedID是一个带有需要ID号的雇员
    ...
    EmpIDSet::iterator i =
    	se.find(selectedID);			// 第一步:找到要改变的元素
    if (i!=se.end()){
    	Employee e(*i);				// 第二步:拷贝这个元素
    	se.erase(i++);				// 第三步:删除这个元素;
    						// 自增这个迭代器以保持它有效(参见条款9)
    	e.setTitle("Corporate Deity");		// 第四步:修改这个副本
    	se.insert(i, e);				// 第五步:插入新值;提示它的位置
    						// 和原先元素的一样
    }
    

第 23 条:考虑用排序的 vector 替代关联容器。

  • 对于许多应用,非标准的哈希容器可能提供常数时间的查找能力优于 setmultisetmapmultimap的确定的对数时间查找能力。
    • 即使你需要的就只是对数时间查找的保证,标准关联容器仍然可能不是你的最佳选择。(vector 可能效率也更高)
  • 本条款讨论的就是在什么情况下 vector 可以比标准关联容器更快。
    • 当然,是在有序 vector 的前提下才有可能比一个关联容器能提供更高的性能;因为只有有序容器才能正确地使用查找算法:binary_search()lower_bound()equal_range() 等。
  • 讨论一,存储同样数据的前提下,标准关联容器耗费更多的内存(比如需要保存它的 next 节点和 prev 节点);假设数据结构比较大,需要分裂成多个页面,vector 就比关联容器需要更少的页面(即更少的页面错误)。
    • 但实际上关联容器也没有那么不堪,因为 STL 都实现了自定义内存管理去将二叉树的节点都集群在相关的小内存页面集中。(如果某个 STL 实现没有改进树节点中的引用局部性,这些节点会分散在内存空间)
    • 进行二分查找时用 vector 那种内存组织方式会使得页面错误最少。
  • 讨论二,有序 vector 的缺点是它必须保持有序,同时它的插入和删除代价也比关联容器更昂贵。
    • 只有在知道目前数据结构的查找几乎不和插入或删除混合使用时,使用有序 vector 代替关联容器才有意义。
  • 讨论三,当用有序 vector 模拟 set,代码骨架如下:
    vector<Widget> vw;						// 代替set
    ...								// 建立阶段:很多插入,
    								// 几乎没有查找
    sort(vw.begin(), vw.end());					// 结束建立阶段。(当
    								// 模拟一个multiset时,你
    								// 可能更喜欢用stable_sort
    								// 来代替;参见条款31。)
    Widget w;							// 用于查找的值的对象
    ...								// 开始查找阶段
    if (binary_search(vw.begin(), vw.end(), w))...			// 通过binary_search查找
    vector<Widget>::iterator i =
    	lower_bound(vw.begin(), vw.end(), w);			// 通过lower_bound查找
    if (i != vw.end() && !(w < *i))...				// 条款19解释了
    								// “!(w < *i)”测试
    pair<vector<Widget>::iterator,
    	vector<Widget>::iterator> range =
    		equal_range(vw.begin(), vw.end(), w);		// 通过equal_range查找
    if (range.first != range.second)...
    ...								// 结束查找阶段,开始
    								// 重组阶段
    sort(vw.begin(), vw.end());					// 开始新的查找阶段...
    
  • 讨论四,当用有序 vector 代替 map 或者 multimap 时,有些有趣的结论。
    1. map 中存储对象类型是 pair,但是 vector 模拟 map 时需要去掉 const(对 vector 进行排序的时候需要对元素赋值);所以 vector 模拟 map 时存储对象类型是 pair

    2. 当排序 vector 时也必须为 pair 写一个自定义比较函数(这个比较函数是用作比较 value,这就是模拟 map 的行为)。

    3. 同时还需要另外一个比较函数来查找(只作用在 key 上)。

    4. 将上述结论用代码描述:

      // 用有序vector模拟map
      typedef pair<string, int> Data;				// 在这个例子里
      							// "map"容纳的类型
      class DataCompare {					// 用于比较的类
      public:
      	bool operator()(const Data& lhs,			// 用于排序的比较函数
      			const Data& rhs) const
      	{
      		return keyLess(lhs.first, rhs.first);	// keyLess在下面
      	} 
      
      	bool operator()(const Data& Ihs,			// 用于查找的比较函数
      			const Data::first_type& k) const	// (形式1)
      	{
      		return keyLess(lhs.first, k);
      	} 
      
      	bool operator()(const Data::first_type& k,		// 用于查找的比较函数
      			const Data& rhs) const		// (形式2)
      	{
      		return keyLess(k, rhs.first);
      	}
      
      private:
      	bool keyLess(const Data::first_type& k1,		// “真的”
      			const Data::first_type& k2) const	// 比较函数
      	{
      		return k1 < k2;
      	}
      };
      
    5. 有序 vector 模拟 map 的代码骨架和前面模拟 set 的差不多,只是必须把 DataCompare 对象用作比较函数。

      vector<Data> vd;						// 代替map
      ...							// 建立阶段:很多插入,
      							// 几乎没有查找
      sort(vd.begin(), vd.end(), DataCompare());			// 结束建立阶段。(当
      							// 模拟multimap时,你
      							// 可能更喜欢用stable_sort
      							// 来代替;参见条款31。)
      string s;							// 用于查找的值的对象
      ...							// 开始查找阶段
      if (binary_search(vd.begin(), vd.end(), s,
      			DataCompare()))...			// 通过binary_search查找
      vector<Data>::iterator i =
      	lower_bound(vd.begin(), vd.end(), s,
      			DataCompare());			// 在次通过lower_bound查找,
      if (i != vd.end() && !DataCompare()(s, *i))...		// 条款45解释了
      							// “!DataCompare()(s, *i)”测试
      pair<vector<Data>::iterator,
      		vector<Data>::iterator> range =
      			equal_range(vd.begin(), vd.end(), s,
      					DataCompare());	// 通过equal_range查找
      if (range.first != range.second)...
      ...							// 结束查找阶段,开始
      							// 重组阶段
      sort(vd.begin(), vd.end(), DataCompare());			// 开始新的查找阶段...
      

第 24 条:当效率至关重要时,请在 map::operator[] 与 map::insert 之间谨慎做出选择。

  • mapoperator[] 函数与众不同,它与 vectordequestringoperator[] 函数无关,与用于数组的内置 operator[] 也没有关系。

  • 相反,map::operator[] 的设计目的是为了提供添加和更新(add or update)的功能,map::operator[] 返回一个引用。

  • 从语句 m[k] = v 出发讨论 operator[] 工作的原理:

    • 返回一个与 k 关联的值对象的引用,然后 v 赋值给所引用(从 operator[] 返回的)的对象。
    • 当键已存在时,已经有 operator[] 可以用来返回引用的值对象;
    • 但当键未存在时,operator[] 就没有可以引用的值对象,此时它使用值类型的默认构造函数从头开始建立一个,然后 operator[] 返回这个新建立对象的引用。
  • 代码分析如下:

    map<int, Widget> m;
    
    // 第一种情况:用operator[]直接插入
    m[1] = 1.50;// 当前m中没有该映射,是直接插入
    	// 上面这个语句相当于调用operator[]:
    m.operator[](1) = 1.50;
    	// 功能上等价于以下语句:
    typedef map<int, Widget> IntWidgetMap;				// 方便的typedef
    pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
    							// 用键1建立新映射入口和一个默认构造的值对象;
    result.first->second = 1.50;
    
    // 第二种情况:用insert直接插入
    m.insert(IntWidgetMap::value_type(2, 1.50));
    	// 与前面的情况相比,节省了三个函数调用:
    		//Ⅰ-默认构造函数带来的临时对象;
    		//Ⅱ-析构该临时对象;
    		//Ⅲ-调用赋值操作符。
    
    // 第三种情况:用insert来更新
    m[1] = 2.0;
    m.insert(IntWidgetMap::value_type(1, 2.0)).first->second = 2;
    	// 在更新的时候使用insert,也同样要承受额外的构造和析构函数
    
  • 对效率的考虑使我们得出结论:

    • 当向映射表中添加元素时,要优先选用 insert,而不是 operator[]
    • 当更新已经在映射表中的元素的值时,要优先选择 operator[]
  • 最后其实可以写一个函数去判断当前情况究竟是更新还是插入,再根据结果考虑用 operator[] 还是 insert

    template<typename MapType,				// map的类型
    		typename KeyArgType,		// KeyArgType和ValueArgtype是类型参数
    		typename ValueArgtype>		
    typename MapType::iterator
    	efficientAddOrUpdate(MapType& m,
    				const KeyArgType& k,
    				const ValueArgtype& v)
    {
    	typename MapType::iterator Ib = m.lower_bound(k);// 找到k在或应该在哪里,注意这里用了typename
    	if(Ib != m.end() &&		// 如果Ib指向一个pair
    		!(m.key_comp()(k, Ib->first))) {	// 它的键等价于k...
    			Ib->second = v;	// 更新这个pair的值
    			return Ib;			// 并返回指向pair的迭代器
    		}	
    		else{
    			typedef typename MapType::value_type MVT;
    			return m.insert(Ib, MVT(k, v));	// 把pair(k, v)添加到m并返回指向新map元素的迭代器
    		}					// 注意这里是insert的hint版本,后续用MVT(k, v)表明了插入位置
    }	
    

第 25 条:熟悉非标准的哈希容器。

  • 本条款描述的是非标准哈希容器,但在 c++11 中有了散列容器 unordered_setunordered_multisetunordered_mapunordered_multimap等。
    • 要注意元素并不是以排序方式存放的。
    • 所以一开始 SGI 的哈希容器是使用了 equal_to() 作为默认的比较函数的(这与关联容器默认使用的 less() 不同)。
    • SGI 的哈希容器通过测试两个对象是否相等(而不是是否等价)来决定容器中的两个对象是否有相同的值。

你可能感兴趣的:(#,Effective,STL,c++)