princples and practice using c++ ch19 reading note
Table of Contents
1 C++ 14 features
- Mechanism for checking template interface.
2 Changing size
上一章的vector仅使用固定的数组来实现, 难以支持'pushback()', 'resize()'等功能. 本节介绍了如何通过reserve操作实现可以动态改变大小的vector.
上一章的vector长度储存在sz中, 现在增加一个space变量, 用于储存vector的总可用空间(已用空间+空闲空间). 当有新成员放入vector时, 先从空闲空间中找位置, 如果空闲空间已被用完, 再申请一块新的空间. 这样可以减少频繁进行内存申请造成的性能损失.
template<typename T> void Vector<T>::reserve(int newalloc) { if (newalloc < space) return; T *p = new T[newalloc]; for (int i = 0; i < sz; i++) p[i] = elem[i]; delete[] elem; elem = p; space = newalloc; }
template<typename T> void Vector<T>::resize(int newsize) { if (newsize < space) return; reserve(newsize); for (int i = sz; i < newsize; i++) elem[i] = 0; sz = newsize; }
template<typename T> void Vector<T>::push_back(T value) { if ( space == 0 ) reserve(8); if ( space == sz) reserve(2*space); elem[sz] = value; sz ++; }
3 Templates
3.1 Types as templates parameters
template<typename T>
or
template<class T> // include built-in type
编译器会在编译阶段或link阶段进行模板展开
3.2 Generic programming
使用模板是泛型编程的基础.
一般所说的多态包括两种类型:
- parametic polymorphism: 依赖于模板参数(泛型编程)
- hoc polymorphism: 使用类继承, 虚函数(面向对象编程)
需要注意的差异:
- 决定被调用函数的时机: 面向对象编程在运行时决定, 而泛型编程在编译时决定.
两者结合使用的例子:
void draw_all(Vector<Shape*>& v) { for ( int i = 0; i < v.size(); i++ ) v[i]->draw(); }
在泛型的vector中储存shape, 然后调用它们的虚函数draw().
3.3 Concepts
模板虽好, 也有缺点. 模板的内部检查比较薄弱,而且可能在编译晚期才发现问题.
我写了个test case:
template<typename T> class test { public: test(T& v):val{v}{}; T val; T operator+(T v) { return this->val + v; } };
如果类型T支持'+'操作, 一切ok. 但如果T不支持'+', 且调用了test的+方法, 编译器会报错:no mach for operator+.
c++14提供了一套机制来规定对类型T的要求, 叫concepts
最常用的,如果要求T实现了拷贝/移动/默认构造函数, 则在template后追加requires Element.
template<typename T> requires<Element T>
or
template<Element T>
Element是满足一系列条件的集合. 相应的还有许多其它集合:如果要求T可以分配和释放内存, 可以使用Allocator. 如果要求T是容器, 可以使用Container, 等等. 查阅19.3.3.
c++14之前的版本不支持concepts, 只能通过一些约定来限定了.
3.4 Containers and inheritance
两个有继承关系的类型搁到模板类中以后就没有任何关系了, 模板展开后它们是完全不同的两个类型.
下面是错误示范:
vector<Shape> vs; vector<Circle> vc; vs = vc; // error void f(vector<Shape> &); f(vc); // error
3.5 Integers as template parameters
除了类型外, 模板还可以传递其它参数, 最常用的是int.
template<typename T, int N>
其它类型参数不太常用, 并且需要开发者非常熟悉语言特性.
3.6 Template argument deduction
对函数模版参数来说, 当编译期能够通过函数参数确定模板参数的值, 通常可以不显示书写模板参数.
template<typename T, int N> fill(array<T,N>& a, T& v); array<double,10> d; fill(d,0); // 相当于 fill<double,10>(d,0);
3.7 Generalizing vector
现在我们的vector离实用还差一些. 比如以下两个问题:
- 如果Vector<X>的X没有默认值咋办?
- 怎么保证当vector销毁时, 它包含的所有元素也能被销毁?
对于第一个问题, 可以让用户提供默认值:
template<typename T>void vector<T>::resize(int newsize, T def = T());
如果用户不提供默认值, 则使用T().
对于第二个问题, 解决方案是使用allocator:
template<typename T> class allocator { public: T* allocate(int n); // 分配n个T的内存 void deallocate(T*, int n); // 回收n个T的空间 void construct(T*, const T&); // 拷贝一个T void destory(T*); // 回收T
4 Range checking and exceptions
边界检查使程序更健壮, 但降低效率.
标准容器会提供带有边界检查的索引方式 at(), 也会提供快速的, 不检查边界的索引 operator[]().
5 Resource and exceptions
resource需要被申请和释放, 常用的资源包括: Memory, Locks, File handles, Thread handles, Sockets, Windows.
5.1 Potential resource management problems
下面以memory为例进行说明.
new和delete应该是成对出现的, 然而它们之间会发生什么奇怪的事情就不知道了:
int p* = new p[10]; // p = q; // p 可能指向了别处 // return; // 程序可能已经返回 // try{..} catch{..} // 可能抛出异常 delete[] p;
为了确保p能够被释放, 一个稍微好一点的版本可能长这样:
int p* = new p[10]; try { ... } catch() { delete[] p; return; } delete[] p;
虽然有点搓, 但至少解决了温饱. 那么问题来了, 如果我们有一大波p需要被delete怎么办.
5.2 Resource acquisition is initialization
接上节. 当面临一大波p时, 好在还有vector:
void f() { vector<int> p(10); vector<int> q(10); }
这样我们就不用担心delete的问题了:
- vector初始化函数负责new,析构函数中delete.
- p不是指针,不必担心中途被改变.
- 离开作用域时,所有fully-constructed object(以及sub-object)的析构函数自动被调用.
引用一下百度百科:
RAII (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
5.3 Guarantee
一个常见的场景, 我们可能希望在在作用域之外使用p:
void make_vec() { vector<int> p = new vector<int>(); // ... return p; }
同delete的问题类似, p可能活不到被return. 类似的, 这里也可以使用try-catch来解决. 这就是所谓的basic-guarantee.
- basic-guarantee: 确保没有内存泄露.
- strong-guarantee: 在basic的基础上, 还要确保所有observable value(非本地变量)的值不变, 除了抛出异常外. 这样的函数是可重入的.
- no-throw-guarantee: 足够安全,不throw. 所有c++内置的工具提供no throw guarantee, 只要开发者不使用throw,new,dynamiccast这三种操作.
5.4 uniqueptr
try-catch还是太丑了. 这里介绍一个更牛逼的RAII工具, <memory>的uniqueptr.
vector<int> *make_vec() { unique_ptr<vector<int>> p {new vector<int>}; // fill ..., may throw a exception. return p.release(); }
uniqueptr 是一个拥有指针的实例, 它被析构时(离开makevec的作用域), 会负责销毁所拥有的指针. p.release()把vector从p中解绑, 这样以后p会指向一个nullptr, 被销毁时不会释放掉vector.
最好不要在uniqueptr中嵌套uniqueptr.
5.5 Return by moving
对容器来说,有一种更加优雅的解决方案: 使用move constructor传递内容.
void make_vec() { vector<int> m; // ... return m; // the move constructor efficiently transfers ownership. }
5.6 RAII for vector
如果使用了smart pointer - 比如uniqueptr, 问题依旧存在..
- 怎样保证所有的pointer都被保护起来了?
- 如果有些实例在退出作用域时不需要被销毁怎么办? (你烦不烦…
参考前面使用allocator的reserve实现.
其中, alloc.construct(&p[i], elem[i])可能会抛异常, 这样后面的alloc.destory(&elem[i])就执行不到了..似曾相识的状况..
好一点的解决方案是:把vector的memory(包括sz,elem,space)当做resource:
struct vector_base { A alloc; T* elem; int sz; int space; vector_base(const A& a, int n); ~vector_base(); }
vector可以继承vectorbase
class vector: private vector_base<T,A>
重新实现reserve():
void vector<T,A>::reserve(int newalloc) { if ( newalloc <= this->space ) return; vector_base<T,A> b(this->alloc, newalloc); uninitialized_copy(b.elem, &b.elem[this->sz], this->elem); // copy for ( int i = 0; i < this->sz; i++ ) this->alloc.destory(&this->elem[i]); swap<vector_base<T,A>>(*this, p); }
uninitializedcopy可以处理掉拷贝构造函数抛出的异常. 如果中间抛了异常, 则新的p会在离开作用域时被析构.
有一点要注意, 这里*this和p是不同类型, 所以在调用swap时需要显示指定一下类型.
- 以上