如何设计 C++ STL 风格容器

STL标准库所提供的容器已经足够大多数的应用, 并且足够地稳定。但是当我们需要造点轮子的话,相比起另起炉灶, 能与现有的C++语义和容器兼容更加好. 我总结了一下一个STL-style container所应做的事情.

假设我们的容器类是:

template<
    typename T,
    typename Allocator = std::allocator<T>
> class my_container;

代码风格

很多人表达过对STL代码风格的不满(全都是下划线), 至于libstdc++里满屏满屏的__gnu_cxx_****简直就是在防盗版. 不过也单独拿出来说一说, 并且拿其它的风格对比一下:

  • 保护型别用_St, 大多公用型别带有some_type后缀.

  • 模板中的参数名用CamelCase, 文档中称为 Concept .

  • 保护成员变量和函数用_M_var_or_func. Boost和Google的代码风格建议用var_or_func_.

  • __参数和局部变量__命名 __arg_or_local 都毫不留情地用两条下划线开头. Boost不建议用下划线.

Operations (操作)

对于数据结构的一些操作, 你应该设计相应的运算子(其实是语法糖)令程序更可读:

  • 一个好的容器应该起码要有重分配的操作, assignoperator=

  • 对于一些随机存取容器, at 对应 operator[], 但是当不存在时, at不会分配新元素而operator[]会.

  • 对于一些线性表, push_backpush 可以对应 operator+=.(但是并没有哪个容器这么做过, 为什么?)

  • 比较运算符operator==,!=,<,<=,>,>=, 对于元素类型满足EqualityComparable等概念的容器, 逐个元素迭代逐个元素比较.

然后是一些关于操作的约定命名:

  • 你不应该用 deleteremove 來代替 erase. erase 应该返回新的迭代器以告知用户旧的迭代器失效.

  • 你不应该用 isempty(用is表示它是个谓词) 來代替 empty(虽然这会带来歧义).

  • 你不应该用 empty 來代替 clear.

  • 你不应该用 first(更多用于元组)或 head 來代替 front

  • 你不应该用 lasttail 來代替 back

  • 你不应该用 search(std::search是搜寻一串连续元素时用的) 來代替 find, 并在找不到时返回end()(rfind返回rend())

并且你应该尽量实现这些操作.

Iterator (迭代器)

迭代器用于遍历, 这是一个容器所必要的. 一个容器的迭代器至少应该有begin()end()两个接口來获__第一个元素__以及所谓__最后一个元素的后继元素__的迭代器, 通常还应该有cbegin()cend()來保证获取到常量版本. 迭代器的分类如果是BidirectionalIterator, 你还应该实现反向迭代器rbegin(), rend()和它们的常量版本. 事实上, STL (的一些实现) 非常依赖迭代器, 如vector的operator[], 甚至将定位完全交给迭代器的算法去解决:

// class vector { ... public:
reference operator[](size_type __n) { return *(begin() + __n); }

迭代器本身应该继承自std::iterator以保证5个特性(Trait)完整. 留意我们如果要获取一个容器的迭代器特性, 不应该直接 my_iterator::iterator_category, 因为在一些实现中my_iterator可能是vanilla pointer, 而应该 std::iterator_traits<my_iterator>::iterator_category, 这有助于减慢编译器的编译速度和增加代码长度秀逼格(误):

  • iterator_category: 迭代器分类, 指定一个iterator_tag. 用于在迭代的时候使用__更合适的重载函数__.

  • value_type

  • pointer

  • reference

  • difference_type: 这应当是一个数值类型, 当你调用i += nn的类型.

当你要为你的BidirectionalIterator实现反向迭代器的时候, 大可不必亲自动手造轮子. 利用标准库提供的__适配器__, 你只需要简单地令:

typedef std::reverse_iterator<my_iterator> reverse_iterator;
reverse_iterator rbegin() { return reverse_iterator(end()); }

Allocator (分配器)

或许你在教科书上得知, 你可以在constructor里new一段内存, 并且在destructor里delete这段内存, 并且深深为面向对象的优雅所折服, 认为这是最好的做法. 然而更好的做法是, 我们把分配内存的工作从类的设计中独立出来, 交给这个叫分配器的东西帮我们处理.

一个容器最基本的功能就是储存功能, 所以容器的基本特性, 事实上就是帮助管理空间的分配器的基本特性. 分配器有7个由T生成的7个特性, 可以用allocator_traits获取. 例如容器的型别我们应该这样定义:

typedef std::allocator_traits<allocator_type>::const_pointer const_pointer;
typedef std::allocator_traits<allocator_type>::const_reference const_reference;

关于更多的Traits这里不详述, 它主要目的是通过模板__偏特化__(又叫部分特例化, partial specialization)来同时为C时代就有的传统类型和class提供__统一的接口__为程序编写提供特性信息.

C++的分配器是无状态的, 它只有分配功能帮你省去各种烦人的类型转换和类型问题, 而不能代替smart pointer之类的进行安全的内存管理. 你在管理内存的过程中, 你应该至少这样做并不限于 (分配器的具体接口参考标准库文档):

  • p = allocator_.allocate(n) 而不是 p = static_cast<value_type*> (operator new (n * sizeof(value_type))).

  • allocator_.construct(p) 而不是 p = new(p) value_type.

C++11

C++11 新增了一些特性, 一个现代的容器应该与标准保持兼容.

initializer_list

在构造函数里实现初始化列表可以使你的容器拥有类似这样优美的语义:

my_container<int> cont{1, 3, 5, 7};
my_container<pair<int, int>> = {{3, 4}, {5, 6}};

// 要做到这一点只需要实现这样的接口:
explicit my_container(std::initializer_list<value_type> l) { /*...*/ }

你可以在类似于 append() , assign() 等所有你认为合适的地方也实现这个接口.

R-value Reference

移动语义解决了饱受诟病的右值构造的浪费问题. 你需要在构造函数, assign(), operator=() 实现右值引用版本的接口.

my_container(const self_type& other) { /* copy other here */ }
// 除了经典的复制构造函数以外, 你还应该实现右值引用版本的重载
my_container(self_type&& other) { /* swap pointers */ }

转发解决了参数传递时多次构造的浪费问题, 这可能在 insert(), push_back()之类的函数中用到. 比如下面, 转发可以令这个vector在整个插入过程中仅构造了一次.

my_container<vector<int>> cont;
cont.append(vector(1, 2, 3, 4));
cont.append({1, 2, 3, 4});

// 利用模板甚至可以用一个函数含括这两种操作
template <typename ... Args>
void append(Args&& ... args) {
    // ...
    allocator_.construct(p, std::forward<Args>(args)...);
}

Thread Safe

C++11很有绅士风度地加进了线程库, 并且对STL的线程安全实现提出如下要求:

  • 保证对读同一个容器的并发安全

  • 保证写不同容器时的并发安全

什么!? 这跟没有线程安全有什么区别? 是的, 然而这并不是一个Bug, 而是一个Feature, 这一切都是为了通用库性能考虑.


暂时先写到这, 以后想到什么再加. 本人水平有限, 此文权当抛砖引玉, 欢迎大家指正.

最近居委会,啊不是委员会释出了一份材料:C++ Core Guidelines。大家可以参考参考。

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值