org-page

static site generator

princples and practice using c++ ch19 reading note

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时需要显示指定一下类型.

  • 以上

Comments

comments powered by Disqus