[C++]高效使用关联容器的一些建议

关联容器

本文介绍在关联容器中常见的一些的问题以及提升使用关联容器的建议。

1. 理解相等(equality)和等价(equivalence)的区别。

相等是以operator==为基础的。等价是以operator<为基础的。

例如find的定义是相等,他用operator==来判断,这是比较容易理解的。

而等价关系是以“在已排序的区间中对象值的相对顺序”为基础的。也就是说,如果两个值中任何一个(按照既定的排列顺序)都在另一个的前面,那么他们就是等价的。

    !(w1 < w2) && !(w2 < w1);
    !c.key_comp()(x, y) && !c.key.comp(y, x);

key_comp()返回一个函数,并以x, y作为参数传入。

具体而言,我们设想一个不区分string大小的set,来具体说明问题。

#include <iostream>
#include <set>
#include <string>
using namespace std;
bool ciCharLess(char c1, char c2) {
// 在C++中,char可能是有符号的也可能是无符号的,所以我们要转换为无符号的char,才能比较。
    return tolower(static_cast<unsigned char> (c1)) <
            tolower(static_cast<unsigned char> (c2));
}
bool ciStringCompare(const string& s1, const string& s2) {
    return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
}
// It must be a struct but not be a class.
struct CIstring : public binary_function<string, string, bool> {
    bool operator() (const string& T1, const string& T2) const {
        return ciStringCompare(T1, T2);
    }
};
int main(int argc, char *argv[]) {
    set<string, CIstring> ciss;
//  ciss.insert("yan");
    ciss.insert("Yan");
    ciss.insert("yan");
    copy(ciss.begin(), ciss.end(), ostream_iterator<string>(cout, "\n"));
    if (ciss.find("yan") != ciss.end()) {
        // base on equivalence.
        cout << "find" << endl;
    }
    if (find(ciss.begin(), ciss.end(), "yan") != ciss.end()) {
        // base on equality.
        cout << "find, again!" << endl;
    }
    return 0;
}
/*
Yan
find
*/

当你看到输出情况后是否发现了问题?同样是find,作为成员函数的find是用基于等价的比较函数,而非成员函数则是用基于相等的比较函数,所以他们会出现不一样的结果。

从技术实现角度来讲,定义了 operator == 运算符的类型都属于Equality Comparable 。例如,C++中所有内置类型和指针类型都是Equality Comparable 概念下的类型。C++ STL中find、adjacent_find、find_first_of、search、find_end、search_n、count、 equal、mismatch、replace、replace_copy、remove、remove_copy、unique、 unique_copy等函数(如果有重载,均指非传入函数对象版本)要求元素类型属于Equality Comparable ,即要求该类型定义有 operator == 运算符。

从技术实现角度来讲,定义了 operator < 运 算符的类型 都属于Strict Weakly Comparable 。例如,C++中所有内置类型和指 针类型都是LessThan Comparable概念下的类型。C++ STL中next_permutation、prev_permutation、sort、stable_sort、partial_sort、 partial_sort_copy、nth_element、binary_search、lower_bound、upper_bound、 equal_range、merge、inplace_merge、includes、set_union、set_intersection、 set_difference、set_symmetric_difference、makde_heap、push_heap、pop_heap、 sort_heap、等函数(如果有重载,均指非传入函数对象版本),以及set、map、multiset、multimap、 priority_queue等容器类,都要求元素类型属于Strict Weakly Comparable ,即要求该类型定义有 operator < 运算符。

摘自C++ STL的几种常用“比较”概念简述

2. 为包含指针的关联容器指定比较类型

对于包含了指针的关联容器,几乎毫无疑问一定自己设置比较函数的子类,不然会出现意想不到的结果。另外需要注意的是,创建容器的参数都必须是类型,不能是函数,所以你一定放入一个函数对象,让STL在内部实现时调用这个函数。

此处直接给出正确的做法。不考虑内存泄露。

#include <iostream>
#include <set>
#include <string>
using namespace std;
struct Compare : public binary_function<const string*, const string*, bool> {
    bool operator()(const string* s1, const string* s2) {
        return *s1 < *s2;
    }
};
int main(int argc, char *argv[]) {
    set<string*, Compare> s;
    s.insert(new string ("c"));
    s.insert(new string ("b"));
    s.insert(new string ("a"));
    return 0;
}

然后,如果希望把容器内信息输出有几种不同的写法。

1 . 使用transform把容器内对象转换为引用然后输出给ostream_iterator.

struct Deference {
    template<typename T>
    const T& operator()(const T* ptr) const {
        return *ptr;
    }
};
    transform(s.begin(), s.end(), ostream_iterator<string>(cout, "\n"), Deference());

2 . 使用for_each函数,然后用匿名函数。这里我发现原来匿名函数里不能使用auto,不然的话,就不需要判断容器内的类型了。

    for_each(s.begin(), s.end(), [](string* s) {
        cout << *s << endl;
    });

3. 总是让对象的比较函数在相等时返回false

这是一个和小的细节,但是却会引发很大的问题。

例如在set中,每次输入都会比较要输入的值是否与原有的值等价,如此此时判断相等却给出了true,那么元素就会被插入进容器中,破坏容器中的数据结构。就算是在multiset也是一样会导致问题,例如在调用equal_range时,会因为比较函数而给出不正确的结果。

所以,如果你视图在比较函数中对相等的值返回true,你会破坏所有的标准关联容器,不管它是否保存相同的值。

4. 切勿直接修改set或muitiset中的键

请注意,map和muitimap在许多实现中key是const类型,是不允许被修改的,如果尝试修改他的键会直接导致不可移植性。但是对于set却不是这样的,他允许被修改,但必须注意的是,不能修改key。之所以不能修改set里面的key,是因为每次修改都会影响整个set的排列结构。

对于某些实现来说,set的key确实是const的。如果你非得要修改key,可以使用以下方法。

#include <iostream>
#include <set>
#include <string>
using namespace std;
class Employee {
public:
    const string& name() const;
    void setname(const string& name);
    const string& title() const;
    void settitle(const string& title);
};
struct names : public binary_function<Employee, Employee, bool> {
    bool operator()(const Employee& lhs, const Employee& rhs) const {
        return lhs.name() < rhs.name();
    }
};
int main(int argc, char *argv[]) {
    typedef set<Employee, names> EmpSet;
    EmpSet se;
    ...
    Employee selectTitle;
    EmpSet::iterator i = se.find(selectTitle);
    if (i != se.end()) {
        // i->setname(...); 是不可移植的。
        const_cast<Employee&> (*i).setname("yan");
    }
    return 0;
}

我们在这里设定set排序的key,并且尝试去改变他的key。需要注意的是const_cast<Employee&> (*i).setname("yan");的写法。

通过改变他的const属性,把他转变为Employee&。 原因在于如果只是把他改为Employee实际上只是获得他的一个复制对象,并不能改变他原来的对象。

至于如何修改map的键值。其实很简答。只要把原来的那个pair删除,重新加入一个新的修改了key的pair。

5. 当效率很重要时,请在map::operator[]和map::insert之间做出选择

map::operator[]和map::insert都能够用于更新和插入新的值,但是在效率上有很大的区别。

简单的说,map::operator[]适用于更新值。原因在于,

class Widget {
    public:
        Widget() {
            cout << "constructed!" << endl;
        }
        Widget(double i) {
            cout << "copy" << endl;
        }
        Widget& operator=(double i) {
            cout << "pass"  << endl;
            return *this;
        }
};

int main(int argc, char *argv[]) {
    typedef map<int, Widget> IntWidgetMap;
    IntWidgetMap m;
    m[0] = 1.4;
    return 0;
}
/*
constructed!
pass
*/

实际上是这样实现的:

class Widget {
    public:
        Widget() {
            cout << "constructed!" << endl;
        }
        Widget(double i) {
            cout << "copy" << endl;
        }
        Widget& operator=(double i) {
            cout << "pass"  << endl;
            return *this;
        }
};

int main(int argc, char *argv[]) {
    typedef map<int, Widget> IntWidgetMap;
    IntWidgetMap m;
    pair<IntWidgetMap::iterator, bool> result = m.insert(IntWidgetMap::value_type(1, Widget()));
    result.first->second = 1.50;
    return 0;
}
/*
constructed!
pass
*/

而适用insert函数就高效许多。需要注意的是一定要使用value_type。这总是某种形式的pair。

int main(int argc, char *argv[]) {
    typedef map<int, Widget> IntWidgetMap;
    IntWidgetMap m;
    m.insert(IntWidgetMap::value_type(1, 1.3));
    return 0;
}
/*
copy
*/

而对于插入,我们总是倾向于适用operator[]。比较他们的函数调用即可知。

#include <iostream>
#include <map>
using namespace std;

class Widget {
    public:
        Widget() {
            cout << "constructed!" << endl;
        }
        Widget(double i) {
            cout << "copy" << endl;
        }
        Widget& operator=(double i) {
            cout << "pass"  << endl;
            return *this;
        }
};

int main(int argc, char *argv[]) {
    typedef map<int, Widget> IntWidgetMap;
    IntWidgetMap m;
    m.insert(IntWidgetMap::value_type(1, 1.3));
    cout << endl;
    m[1] = 1.5;
    cout << endl;
    m.insert(IntWidgetMap::value_type(1, 1.5)).first->second = 10;
    return 0;
}
时间: 2024-10-20 11:05:19

[C++]高效使用关联容器的一些建议的相关文章

Effective STL --关联容器

高效STL-关联容器 标准关联容器中最重要的就是基于等价而不是相等.比如对于基本的函数库有find函数,但是对于set关联容器也是有find成员函数的.因为标准关联容器保持有序,所以每一个容器必须有一个定义了怎么保持东西有序的比较函数(默认是less).等价是根据这个比较函数定义的,所以标准关联容器的用户只需要为他们要使用的任意容器指定一个比较函数 必须为指针的关联容器指定比较类型 一定要明确对于一个容器来说,容器内的迭代器是存入该容器对象的指针,比如一个关联容器存入指针类型,那么使用这个容器的

[C++]高效使用容器的一些建议

高效使用容器的一些建议 本文介绍一些在使用容器中常见的问题,并给出其解决方法从而提升对容器的认识和使用. 1. 不要试图编写独立于容器类型的代码 STL是以泛化原则为基础的:数组被泛化为"以其包含的对象的类型为参数"的容器:函数被泛化为"以其使用的迭代器的类型为参数"的算法:指针被泛化为"以其指向的对象的类型为参数"的迭代器. 如果我们试图编写独立于容器类型的代码,例如编写一个既能够满足序列容器又满足关联容器的代码,我们最后会发现我们使用的只是他

如何构建高效自主的容器云交付平台?

高效自主的容器化交付平台=敏捷工程理念 x七牛云交付平台组件(云存储+大数据+容器云) 随着 DevOps 理念的普及,大部分公司已经尝试敏捷项目管理并取得一定的成果,但实际代码生产过程仍然是分角色的瀑布式交付,无法实时开发.实时测试.实时部署,但随着容器和大数据技术的到来,让每个企业都能拥有一套高效自主的容器化交付平台. 11 月 23 日,七牛云架构师实践日第 32 期以「容器技术的实践与分享」为主题,在成都成功举行.七牛云工程效率部负责人李倩,在会上为大家带来了题为<容器云交付平台-实现真

C++拾遗(七)——关联容器

关联容器(Associative containers)支持通过键来高效地查找和读取元素.两个基本的关联容器类型是 map 和set.map 的元素以键-值(key-value)对的形式组织:键用作元素在 map 中的索引,而值则表示所存储和读取的数据.set仅包含一个键,并有效地支持关于某个键是否存在的查询.set 和 map 类型的对象所包含的元素都具有不同的键,不允许为同一个键添加第二个元素.如果一个键必须对应多个实例,则需使用 multimap 或 multiset,这两种类型允许多个元

初探STL之关联容器

关联容器 分类:set, multiset, map, multimap 特点:内部元素有序排列,新元素插入的位置取决于它的值,查找速度快. 常用函数: find: 查找等于某个值 的元素(x小于y和y小于x同时不成立即为相等) lower_bound : 查找某个下界 upper_bound : 查找某个上界 equal_range : 同时查找上界和下界 count :计算等于某个值的元素个数(x小于y和y小于x同时不成立即为相等) insert: 用以插入一个元素或一个区间 set 特点:

CH11 关联容器

关联容器与顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的,而顺序容器是按它们在容器中的位置来顺序保存和访问的.两个主要的关联容器:map和set map 中的元素的是一个key-value对:关键字是用来索引值关联的数据.set:每个关键字值包含一个关键字. 关联容器类型: map 保存key-value  map<key,value> set 保存关键字key set<key> multimap 关键字可以重复出现的map multiset 关键字可以重复出现的

STL2——关联容器

关联容器 关联容器支持通过键(key)来高效地查找和读取元素.两个基本的关联容器是map和set,map的元素以键-值对形式组织,键用做索引,值表示存储和读取的数据,set包含一个键,并有效地支持关于某个键是否存在的查询. 引言:pair类型 #include<utility> pair<int,int>p; p.first=5; p.second=6; pair<int,int>p2; p2=make_pair(5,7); pair<int,int>p3(

STL之关联容器

关联容器支持高效的关键字查找和访问.两个主要的关联容器(associative-container)类型是map和set.标准库提供8个关联容器,它们的不同体现在三个维度上: 或者是一个set,或者是一个map 或者要求不重复的关键字,或者允许重复关键字 按顺序保存元素,或无序保存. 允许重复关键字的容器的开头名字中都包含单词multi:不保持关键字按顺序存储的容器的名字都以单词unordered开头. 类型map和multimap定义在头文件map中:set和multiset定义在头文件set

C++关联容器&lt;map&gt;简单总结

C++关联容器<map>简单总结 map提供大小可变的关联容器,基于关联键值高效检索元素值.当你处理键值对的数据是,都可以考虑使用map关联容器. 特点: 大小可变的关联容器,基于关联键值高效检索元素值. 可逆,因为它提供双向迭代器来访问其元素. 有序,因为它的元素根据指定的比较函数按键值排序. 唯一. 因为它的每个元素必须具有唯一键. 关联容器对,因为它的元素数据值与其键值不同. 模板类,因为它提供的功能是一般性的功能,与元素或键类型无关. 用于元素和键的数据类型作为类模板以及比较函数和分配