STL三十条
+ -
管理员 VIP用户 编辑
2023-10-16 0 0

条款20:为指针的关联容器指定比较类型

假定你有一个string*指针的set,你把一些动物的名字插入进set:

set<string*> ssp;                        // ssp = “set of string ptrs”
ssp.insert(new string("Anteater"));
ssp.insert(new string("Wombat"));
ssp.insert(new string("Lemur"));
ssp.insert(new string("Penguin"));

然后你写了下列代码打印set的内容,希望字符串按字母顺序出现。毕竟,确定set保持它们的内容有序。

for (set<string*>::const_iterator i = ssp.begin();        // 你希望看到
            i != ssp.end();            // 这个:“Anteater”
            ++i)                // “Lemur”,“Penguin”,
    cout << *i << endl;                // “Wombat”

注释描述了你希望看见的,但你根本没看见。取而代之的是,你看见四个十六进制的数。它们是指针的值。因为set容纳指针,*i不是一个string,它是一个string的指针。让这成为提醒你坚持条款43的指导并避免自己写循环的一课。如果你已经改为调用copy算法,

copy(ssp.begin(), ssp.end(),                // 把ssp中的字符串
    ostream_iterator<string>(cout, "\n"));        // 拷贝到cout(但这
                            // 不能编译)

你将不仅打更少的字符,而且你将很快会查明你的错误,因为这个copy的调用将不能编译,ostream_iterator需要知道被打印的对象的类型,所以当你告诉它是一个string时(通过作为模板参数传递),编译器检测到那和ssp中储存的对象类型(是string*)之间不匹配,它们会拒绝编译代码。获得了额外的类型安全。

如果你愤怒地把显式循环中的*i改为**i,你可能可以得到你想要的输出,但也可能不。是的,动物名字将被打印,但它们按字母顺序出现的机会只是24份之1。ssp保持它的内容有序,但是它容纳的是指针,所以它以指针的值排序,而不以string值。对于四个指针值可能有24种排列(译注:4! = 4 * 3 * 2 * 1 = 24),所以指针被储存时有24种可能的顺序。因此你看见字符串按字母排序有24份之1的几率。[1]

为了克服这个问题,你应该回忆起

set<string*> ssp;

是这个的简写:

set<string*, less<string*> > ssp;

好,为了完全准确,它是

set<string*, less<string*>, allocator<string*> > ssp;

的简化,但是分配器在本条款里与我们无关,所以我们将忽略它们。

如果你想要string*指针以字符串值确定顺序被储存在set中,你不能使用默认比较仿函数类less<string*>。你必须改为写你自己的比较仿函数类,它的对象带有string*指针并按照指向的字符串值来进行排序。就像这样:

struct StringPtrLess:
    public binary_function<const string*,        // 使用这个基类
                const string*,        // 的理由参见条款40
                bool> {
    bool operator()(const string *ps1, const string *ps2) const
    {
        return *ps1 < *ps2;
    }
};

然后你可以使用StringPtrLess作为ssp的比较类型:

typedef set<string*, StringPtrLess> StringPtrSet;
StringPtrSet ssp;                    // 建立字符串的集合,
                        // 按照StringPtrLess定义的顺序排序

...                        // 和前面一样插入
                        // 同样四个字符串

现在你的循环最后将做你想要它做的(也就是前面你使用*i代替**i所修正的问题):

for (StringPtrSet::const_iterator i = ssp.begin();    // 打印“Anteater”,
        i != ssp.end();            // “Lemur”
        ++i)                // “Penguin”
    cout << **i << endl;            // “Wombat”

如果你想要改为使用算法,你可以写一个知道怎么在打印string*指针之前对它们解引用的函数,然后和for_each联用那个函数:

void print(const string *ps)            // 把ps指向的
{                        // 对象打印到cout
    cout << *ps << endl;
}

for_each(ssp.begin(), ssp.end(), print);        // 在ssp中的每个
                        // 元素上调用print或者你想象并写出了泛型的解引用仿函数类,然后让它和transform与ostream_iterator连用:

// 当本类型的仿函数被传入一个T*时,它们返回一个const T&
struct Dereference {
    template <typename T>
    const T& operator()(const T *ptr) const
    {
        return *ptr;
    }
};

transform(ssp.begin(), ssp.end(),                // 通过解引用“转换”
        ostream_iterator<string>(cout, "\n"),    // ssp中的每个元素,
        Dereference());                // 把结果写入cout

但是,用算法代替循环不是要点,至少对于本条款来说是这样的。(它是条款43的要点。)要点是无论何时你建立一个指针的标准关联容器,你必须记住容器会以指针的值排序。这基本上不是你想要的,所以你几乎总是需要建立自己的仿函数类作为比较类型。

注意到我写的是“比较类型”。你可能奇怪为什么必须特意创造一个仿函数类而不是简单地为set写一个比较函数。例如,你可能想试试:

bool stringPtrLess(const string* ps1,        // 将成为用于
            const string* ps2)        // 按字符串值
{                        // 排序的string*指针
    return *ps1 < *ps2;            // 的比较函数
}

set<string*, stringPtrLess> ssp;            // 假设使用stringPtrLess
                        // 作为ssp的比较函数;
                        // 这不能编译

这里的问题是每个set模板的第三个参数都是一种类型。令人遗憾的是,stringPtrLess不是一种类型,它是一个函数。这就是为什么尝试使用stringPtrLess作为set的比较函数不能编译的原因,set不要一个函数,它要的是能在内部用实例化建立函数的一种类型。

无论何时你建立指针的关联容器,注意你也得指定容器的比较类型。大多数时候,你的比较类型只是解引用指针并比较所指向的对象(就像上面的StringPtrLess做的那样)。鉴于这种情况,你手头最好也能有一个用于那种比较的仿函数模板。像这样:

struct DereferenceLess {
    template <typename PtrType>
        bool operator()(PtrType pT1,        // 参数是值传递的,
                PtrType pT2) const        // 因为我们希望它们
        {                    // 是(或行为像)指针
            return *pT1 < *pT2;
        }
};

这样的模板消除了写像StringPtrLess那样的类的需要,因为我们可以改为使用DereferenceLess:

set<string*, DereferenceLess> ssp;        // 行为就像
                    // set<string*, StringPtrLess>

噢,还有一件事。本条款是关于指针的关联容器,但它也可以应用于表现为指针的对象的容器,例如,智能指针和迭代器。如果你有一个智能指针或迭代器的关联容器,那也得为它指定比较类型。幸运的是,指针的这个解决方案也可以用于类似指针的对象。正如DereferenceLess适合作为T*的关联容器的比较类型一样,它也可以作为T对象的迭代器和智能指针容器的比较类型。


[1] 实际上,这24种排列很可能不是平等的,所以“24份之1”的陈述有点使人误解。确实,有24个不同的顺序,而且你可能得到它们中的任何一个。

插入辅文

条款1:仔细选择你的STL容器
你知道C++中有很多你可以支配的容器,但是你意识到有多少吗?要确定你没有忽略你的选项,这里有一个快速回顾。标准STL序列容器:vector、string、deque和list。 标准STL关联容器:set、multiset、map和multimap。 非标准序列容器slist和rope。slis......
条款2:小心对“容器无关代码”的幻想
STL是建立在泛化之上的。数组泛化为容器,参数化了所包含的对象的类型。函数泛化为算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。这只是个开始。独立的容器类型泛化为序列或关联容器,而且类似的容器拥有类似的功能。标准的内存相邻容器(参见条款1)都提供随机访问迭代器,标准......
条款3:使容器里对象的拷贝操作轻量而正确
容器容纳了对象,但不是你给它们的那个对象。此外,当你从容器中获取一个对象时,你所得到的对象不是容器里的那个对象。取而代之的是,当你向容器中添加一个对象(比如通过insert或push_back等),进入容器的是你指定的对象的拷贝。拷进去,拷出来。这就是STL的方式。一旦一个对象进入一个容器,以后对......
条款7:当使用new得指针的容器时,记得在销毁容器前delete那些指针
STL中的容器非常优秀。它们提供了前向和逆向遍历的迭代器(通过begin、end、rbegin等);它们能告诉你所容纳的对象类型(通过value_type的typedef);在插入和删除中,它们负责任何需要的内存管理;它们报告容纳了多少对象和最多可能容纳的数量(分别通过size和max_size);......
条款8:永不建立auto_ptr的容器
坦白地说,本条款不需要出现在《Effective STL》里。auto_ptr的容器(COAPs)是禁止的。试图使用它们的代码都不能编译。C++ 标准委员会花费了无数努力来安排这种情况[1]。我本来不需要说有关COAPs的任何东西,因为你的编译器对这样的容器应该有很多抱怨,而且所有那些都是不能编译的......
条款9:在删除选项中仔细选择
假定你有一个标准STL容器,c,容纳int,Container c;而你想把c中所有值为1963的对象都去掉。令人吃惊的是,完成这项任务的方法因不同的容器类型而不同:没有一种方法是通用的。如果你有一个连续内存容器(vector、deque或string——参见条款1),最......
条款12:对STL容器线程安全性的期待现实一些
标准C++的世界是相当保守和陈旧的。在这个纯洁的世界,所有可执行文件都是静态链接的。不存在内存映射文件和共享内存。没有窗口系统,没有网络,没有数据库,没有其他进程。在这种情况下,当发现标准没有提到任何关于线程的东西时你不该感到惊讶。你对STL的线程安全有的第一个想法应该是它将因实现而不同。当然,多......
条款20:为指针的关联容器指定比较类型
假定你有一个string*指针的set,你把一些动物的名字插入进set:set ssp; // ssp = “set of string ptrs”ssp.insert(new string("Anteater......
条款23:考虑用有序vector代替关联容器
当需要一个提供快速查找的数据结构时,很多STL程序员立刻会想到标准关联容器:set、multiset、map和multimap。直到现在这很好,但不是永远都好。如果查找速度真得很重要,的确也值得考虑使用非标准的散列容器(参见条款25)。如果使用了合适的散列函数,则可以认为散列容器提供了常数时间的查找......
条款25:熟悉非标准散列容器
STL程序员一般用不了多久就开始惊讶,“vector、list、map,很好,但是散列(hash)表在哪里”?唉,在标准C++库里没有任何散列表。 每个人都同意这是个不幸,但是标准委员会觉得需要加给他们的工作可能会过渡地推迟标准的完成。可以肯定标准的下一个版本将包含散列表,但是目前,STL没有散列的......
条款30:确保目标区间足够大
STL容器在被添加时(通过insert、push_front、push_back等)自动扩展它们自己来容纳新对象。这工作的很好,有些程序员因为这个信仰而被麻痹,认为他们不必担心要为容器中的对象腾出空间,因为容器自己可以照顾好这些。如果是那样就好了!当程序员想向容器中插入对象但并没有告诉STL他们所想......
条款33:提防在指针的容器上使用类似remove的算法
在管理一堆动态分配的Widgets,每一个都可能通过检验,你把结果指针保存在一个vector中:class Widget{public: ... bool isCertified() const; // 这个Widget是否通过检验 ...};vector&l......
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!