Effective STL第2条:不要试图编写独立于容器类型的代码
不要试图编写独立于容器类型的代码
引言
在现代C++编程中,标准模板库(STL)以其强大的泛型编程能力著称。它为开发者提供了丰富的容器和算法,极大地提高了代码的复用性和效率。然而,许多开发者在追求代码通用性时,常常陷入试图编写独立于容器类型的代码的误区。这种做法看似追求高效和简洁,但实际上可能导致代码的维护成本增加,性能下降,甚至引入难以调试的错误。
本文将深入探讨为何编写独立于容器类型的代码不可取,并提供一些优化技巧,帮助开发者更高效、更灵活地利用STL。
STL的泛型编程原则
STL的设计基于泛型编程(Generic Programming)原则,其核心思想是通过模板机制将数据结构和算法抽象化。具体来说:
- 容器的泛化:数组被泛化为“以其包含的对象的类型为参数”的容器。例如,
vector<T>、list<T>、set<T>等。 - 算法的泛化:函数被泛化为“以其使用的迭代器的类型为参数”的算法。例如,
find、sort、copy等。 - 迭代器的泛化:指针被泛化为“以其指向的对象的类型为参数”的迭代器。不同容器的迭代器类型不同,支持的操作也不同。
这种泛化设计使得STL具有极高的灵活性和复用性。然而,随着泛化的深入,一些开发者可能会试图编写对所有容器类型都适用的代码,这种做法往往事倍功半。
为何不要编写独立于容器类型的代码
编写独立于容器类型的代码看似追求通用性,但实际上存在诸多问题:
1. 成员函数差异
不同容器类型拥有不同的成员函数。例如:
- 序列容器(如
vector、list、deque)支持push_front、push_back、pop_front、pop_back等操作。 - 关联容器(如
set、map)支持count、lower_bound、upper_bound等操作。
如果试图编写适用于所有容器的代码,必须舍弃这些独特功能,导致代码功能受限。
2. 功能交集的限制
由于不同容器的功能各异,编写通用代码时只能取所有容器功能的交集。例如:
- 所有容器都支持
begin()和end()迭代器,但只有随机访问容器(如vector)支持operator[]。 - 只有
vector和deque支持高效的尾部插入和删除。
这种限制使得通用代码难以充分发挥容器的特性。
3. 迭代器规则差异
不同容器的迭代器失效规则不同:
vector:在插入或删除元素时,迭代器可能会失效。list:插入或删除元素不会导致迭代器失效。set:插入或删除元素会导致指向该元素的迭代器失效。
这种差异使得编写通用代码时需要额外处理,增加了复杂性。
4. 移植性问题
并非所有容器都能将数据传递到C接口中。例如,只有vector支持data()成员函数,可以获取底层数组的指针。试图编写通用代码时,必须考虑这些移植性问题,增加了开发难度。
5. 类型限制
某些容器对存储对象的类型有特殊限制。例如:
- 不能使用
bool作为容器的元素类型(如vector<bool>是一个特化版本,具有不同的行为)。 - 关联容器要求元素类型支持比较操作。
这些限制进一步限制了通用代码的适用性。
类型封装与容器封装
为了避免上述问题,可以采用类型封装和容器封装技术。通过这些技术,可以在不暴露底层容器类型的情况下,简化代码维护。
类型封装
类型封装通过typedef为容器及其迭代器定义别名,从而简化代码修改。例如:
class Widget {};
typedef std::vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer vw;
Widget bestWidget;
WCIterator i = std::find(vw.begin(), vw.end(), bestWidget);
当需要更换容器类型时,只需修改typedef部分:
typedef std::deque<Widget> WidgetContainer;
而无需修改其他部分,极大提高了代码的灵活性和可维护性。
容器封装
容器封装通过将容器封装到一个类中,进一步隐藏底层实现。例如:
class Customer {};
class CustomerList {
private:
typedef std::list<Customer> CustomerContainer;
typedef CustomerContainer::iterator CCIterator;
CustomerContainer customers;
public:
// 提供必要的接口,隐藏容器类型
void addCustomer(const Customer& c) { customers.push_back(c); }
CCIterator begin() { return customers.begin(); }
CCIterator end() { return customers.end(); }
};
通过这种方式,可以在不暴露容器类型的情况下,提供必要的功能。如果需要更换容器类型,只需修改私有部分,而无需修改接口,极大提高了代码的灵活性。
C++11的auto关键字
C++11引入的auto关键字为类型封装提供了更简洁的解决方案。auto允许编译器自动推导变量的类型,从而简化了代码。例如:
class Widget {};
std::vector<Widget> vw;
Widget bestWidget;
// 使用auto简化迭代器类型
auto i = std::find(vw.begin(), vw.end(), bestWidget);
在这里,auto自动识别i的类型为std::vector<Widget>::iterator,从而避免了显式的类型声明。这种简洁性不仅提高了代码的可读性,还减少了代码冗余。
示例代码
以下是结合类型封装和auto关键字的示例代码:
#include <vector>
#include <algorithm>
class Widget {};
typedef std::vector<Widget> WidgetContainer;
WidgetContainer vw;
Widget bestWidget;
// 使用auto简化迭代器类型
auto i = std::find(vw.begin(), vw.end(), bestWidget);
如果需要更换容器类型为std::deque,只需修改typedef部分:
typedef std::deque<Widget> WidgetContainer;
而无需修改其他部分,极大提高了代码的灵活性和可维护性。
结论
STL的泛型编程能力为C++开发者提供了强大的工具,但合理使用容器类型至关重要。试图编写独立于容器类型的代码往往会适得其反,增加开发和维护的复杂性。通过类型封装、容器封装和auto关键字,可以有效简化代码,提高其灵活性和可维护性。
因此,在使用STL时,应根据具体需求选择合适的容器类型,并合理利用封装技术,以充分发挥STL的优势。
更多推荐


所有评论(0)