utilities库——The C++ standard library, 2nd Edition 笔记(三)

目 录

pair<>和tuple<>模板类

智能指针类

numeric limits

type traits, type utilities

辅助函数(min, max, swap, etc.)

ratio<>模板类

时钟和定时器

一些重要的C函数

 

PART I : std::pair 和 std::tuple模板类

一、pair的新特性(相对于C++03)

  • std::pair<T1, T2> p(piecewise_construct, t1, t2) : 用tuple t1和t2的所有元素初始化一个pair。这个版本的构造函数,把tuple t1和t2的成员分别转换为一个初始化器列表,然后传递给pair的first和second成员的构造函数。
  • std::pair<T1, T2> p(rvalue1, rvalue2) : 用右值rvalue1和rvalue2初始化一个pair,rvalue1和rvalue2内容的所有权被转移。
  • get<0> p; <==> p.first;
  • get<1> p; <==> p.second;
  • p = rv; 右值引用赋值,所有权被转移。
  • 如果函数f声明为 void f(std::pair<int, const char*>); 那么,调用时的实参可以是初始化列表:f({3, "three"});
  • make_pair扩展了右值引用重载版本,支持move语义。参数可以使用std::move<T>(para)强制使用右值引用版本,使用std::ref<T>(para)或者std::cref<T>(para)强制使用引用版本或者const 引用版本。例如:
  • std::string s, t;
    auto p = std::make_pair(std::move(s), std::move(t));// string支持move构造,s和t已经没有内容了。
  • 使用tie萃取pair的内容:
    #include <utility>
    #include <tuple>
    
    std::pair<char, char> p {'x', 'y'};
    
    char c;
    std::tie(std::ignore, c) = p; // 把p的第二个元素萃取到c中。
    // std::ignore应用于tuple和pair中,表示被忽略的元素。它是一个空类。

二、std::tuple<>

  • tuple在标准库中的声明 : template<typename... Types> class tuple; // typename...表示Types是变长度模板参数—variadic templates.
  • tuple的操作:
    • tuple的创建:通过显式声明,或者通过make_tuple便捷函数隐式声明;
    • 使用std::get<N>()访问tuple中的第N个元素,N是常量。例子代码:
    • using namespace std;
      tuple<int, float, string> t1(42, 6.3, "Babylon");
      cout << get<0>(t1) << endl; // 输出 42, 宇宙, 生命以及一切问题的终极答案~
      cout << get<1>(t1) << endl; // 输出 6.3;
      cout << get<2>(t1) << endl; // 输出 Babylon;
      
      auto t2 = make_tuple(22, 44, "River");
      get<0>(t1) = get<1>(t2); // 用t2的44为t1的第一个元素赋值;
      
      if (t1 < t2) // tuple可以用来比较;
      {
         t1 = t2; // tuple整体可以用来为另一个tuple赋值;
      }
    • tuple的成员可以是引用:
      std::string s;
      tuple<string&> t(s);
      get<0>(t) = "hello"; // 对s赋值
    • get的模板参数必须能够在编译时确定,并且不能越界。如果t只有三个元素,那么get<3>(t)会引发编译时错误。
  • 便捷函数make_tuple和tie : 
    • make_tuple在不显式指定成员类型时创建一个tuple。如果在make_tuple中指定引用/const引用,需要使用ref()和cref()函数。例如:string s; ... ... auto t = make_tuple(ref(s)); // get<0>(t)将得到s的引用。
    • make_tuple的这种用法可以用来将一个tuple的值取到独立变量当中,方便读取和操作。
      tuple<int, float, string> t(77, 1.1, "morning");
      int i;
      float f;
      string s;
      make_tuple(ref(i), ref(f), ref(s)) = t; // 将t中的值分别取到i, f和s中。也可以用等价的tie函数:
      tie(i, f, s) = t; // 上述用法的便捷方式。tie使用变量的引用来构造tuple。
    • tuple的方法列表:
      tuple<T1, T2, ..., Tn> t(t1, t2, ..., tn); 创建一个指定类型的tuple, 每个模板元素由初始化式指定。
      tuple<T1, T2> t(p); 用pair p初始化两个元素的tuple, p的模板参数必须与tuple一致。
      t = p; 用pair p为一个两元素的tuple赋值, p的模板参数必须与tuple一致。
      ==, !=, >, < >=, <= 关系运算符。
      t1.swap(t2) ; swap(t1, t2); 交换t1和t2的内容。
      make_tuple(v1, v2, ...) 用v1...的值和类型初始化一个tuple, 如果是引用类型,可以用来提取tuple中的元素。
      tie(ref1, ref2, ...)

      构建一个由引用构成的tuple, 可以从其他tuple中把值提取到单个变量中。功能上类似于构建一个由引用构成的struct。

  • tuple与初始器列表:
    • tuple的所有构造函数都是 explicit的,防止将单个变量隐式转换为一个tuple。tuple构造函数有&(引用)和&&(右值引用)两个版本。explicit关键字会导致以下几种后果:
    • 不能用赋值运算去初始化一个tuple,因为那被认为是隐式转换。例如:
      • tuple<int, double> t1(42, 3.14);      // 老的风格;
        tuple<int, double> t2{42, 3.14};      // 新的风格; 初始化器列表;
        tuple<int, double> t3 = {42, 3.14};   // 错误; 需要初始化器列表隐式转换为tuple。
    • 在用到tuple的场合,不能用一个初始器列表代替;而这些格式对于pair是有效的。例如:
      std::vector<tuple<int, float>> v{{1, 1.0}, {2, 2.0}}; //错误,初始化器列表隐式转换为tuple。
      std::tuple<int, int, int> foo() {
          return {1, 2, 3}; // 错误,初始化器列表隐式转换为tuple。
      }
    • 正确的方式是使用make_tuple:
      std::vector<tuple<int, float>> v{make_tuple(1, 1.0), make_tuple(2, 2.0)};
      std::tuple<int, int, int> foo() {
          return make_tuple(1, 2, 3);
      }
  • tuple的其他特性:
    • tuple_size<tupletype>::value  给出tupletype的元素个数。
    • tuple_element<idx, tupletype>::type  给出tupletypeidx个元素的类型。即get<idx>()返回值的类型。
    • tuple_cat()  将多个tuple和pair拼接成一个独立的tuple。
    • typedef std::tuple<int, float, std::string> TupleType;
      // 也可用decltype关键字获取tuple变量的类型;
      std::tuple_size<TupleType>::value // 3
      std::tuple_element<1, TupleType>::type // float;
      
      int n;
      auto tt = std::tuple_cat(std::make_tuple(42, 7.7, "hello"), std::tie(n));
      // tt是一个tuple, 它的前三个元素的类型是int, double和const char*, 最后一个元素的类型是int&(因为调用了tie)。
  • tuple的IO:tuple对<<的支持,是通过元编程中的模板参数递归的方式实现的。具体方法,参考原书pp74.有一个知识点可以关注下:
    • std::tuple<Args...>; ...; sizeof ...(Args); // 返回tuple可变参数的个数。
  • tuple与pair之间的转换:
    • pair可以显式转换为tuple, 前提是二者类型匹配。用一个pair,为类型匹配的tuple赋值也OK。
    • pair有一个接受tuple作为参数的构造函数,tuple可以自由转换为pair. 即piecewise_construct版本的构造函数。
    • pair和array具有与tuple类似的访问接口,例如tie, get<>,tuple_size 和 tuple_element;

PART II: 智能指针类

    C++11提供了两种智能指针。只提供一种智能指针是不充分的,单独一种不能解决所有问题。不同的智能指针为了其智能性做出了不同的取舍,因而具有不同的特性。这两种smart pointer 分别是:

1、shared_ptr:实现了“共享所有权”概念的指针。只有当没有任何shared_ptr指向一项资源时,该资源才会被释放。为了应付更加复杂的场景,C++11还引入了weak_ptr, bad_weak_ptr和enable_shared_from_this等辅助类。

2、unique_ptr:实现了“独享所有权(严格所有权)”概念的指针。该指针保证了任意时刻只有一个智能指针能够指向同一项资源。当然,所有权可以移交。这种指针主要用来避免资源泄漏,比如忘记delete或者发生了exception。

  智能指针声明在<memory>中。

一、class shared_ptr

  shared_ptr提供了这样一种语义:“当资源在任何地方都不再会用到时,清理它”。因此,多个对象可以拥有(共享)同一项资源,而最后一个所有者负责销毁并释放资源。清理动作缺省用delete,但是也可以指定自定义的destuction policy。比如,new[]产生的资源,就必须要用delete []。其它例子还包括:持有句柄、锁、临时文件等资源的释放动作。基本原理可参考《C++ Primer》。

  • 构造:
  • shared_ptr<string> pFoo(new string("Foo")); // OK;
    shared_ptr<string> pFoo = new string("Foo"); // Error; 构造函数被声明为explicit,赋值格式初始化错误。
    shared_ptr<string> pFoo{new string("Foo")}; // OK;

    用于初始化的指针类型不一定与声明完全一致,只要可转换为声明类型即可。也可以使用便捷函数 make_shared<T>()创建shared_ptr。注意,由于make_shared()在一次内存分配中把目标对象和引用计数对象的内存都分配好了,所以时间效率和内存效率会比较高,推荐使用。(关于性能上的实例证明,参考new和make_pair的性能差异比较。)例如:

    std::make_shared<std::pair<int, std::string>>(0, "string"); // make_shared的参数就是目标对象构造函数的参数;参数个数可变。

    也可以先定义一个空的shared_ptr,以后再去给它赋值。但是,赋值不能用operator=,而要用reset方法。

    shared_ptr<string> spPrev; 
    // some code...
    spPrev = new string("Error Assignment"); // Error;
    spPrev.reset(new string("Correct Assignment")); // OK;
  • 引用计数操作:
    • long use_count() : 返回当前共享对象的数量。
    • bool unique() : 返回对象是否只被一个shared_ptr所持有,等价于 use_count() == 1。
  • 其他常方法:
    • template<class D, class T> D* get_deleter(shared_ptr<T>const &p) : 自由函数,获取p的删除器。
    • T* get() const : 成员函数,返回被托管的指针。
  • 修改操作:
    • void swap(shared_ptr& r) : 成员函数,交换指针内容。
    • void reset() : 相当于shared_ptr().swap(*this);
    • template<class Y> void reset(Y *p) : 相当于shared_ptr(p).swap(*this);
    • template<class Y, class D> void reset(Y *p, D d) : 相当于shared_ptr(p, D).swap(*this);
  • 比较函数(自由函数):定义了 operator==, operator !=, operator< 方法,支持有序容器(因为实现了operator<),比较类的算法find, replace等(因为实现了oprerator ==)。
  • 转换函数(自由函数):
    template<class T, class U> shared_ptr<T> static_pointer_cast(shared_ptr<U> const& r);//等价于 shared_ptr<T> (static_cast<T*> (r.get()));
    template<class T, class U> shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const& r);//等价于 shared_ptr<T> (dynamic_cast<T*> (r.get()));
    template<class T, class U> shared_ptr<T> const_pointer_cast(shared_ptr<U> const& r);//等价于 shared_ptr<T> (const_cast<T*> (r.get()));
    • 注意:注释中指出的等价格式,因为没有对参数和返回值的引用计数做同步,会导致二次释放的问题。
  • 指定删除器:
    shared_ptr<int> spArray(new int[10]); //Error! 可能有内存泄露。
    shared_ptr<int> spArray(new int[10], [](int* p){ delete[] p; }); //用lambda指定删除器。
    shared_ptr<int> spArray(new int[10], std::default_delete<int[]>()); //unique_ptr提供的默认删除器。

    shared_ptr的默认删除行为是直接 delete 掉。如果需要有不同的释放行为,需要在构造时指定删除器。删除器是有一个目标对象指针参数的可调用对象;可以是函数指针,lambda, functor等。关于以functor作为删除器,原书提供了一个共享内存的例子,参考P82.

二、class weak_ptr

  weak_ptr 可以应用于两种场合:1、循环引用的两个对象,因为互相持有对方,所以每个对象的引用计数最小为1,永远不会释放,造成内存泄露;2、共享目标对象,但是不管理目标对象的生命周期,即不修改目标对象的引用计数。

  weak_ptr需要一个shared_ptr作为构造参数,当最后一个 shared_ptr 释放掉目标对象后,weak_ptr 自动变为空指针。这样,除了默认构造和拷贝构造,weak_ptr 还提供了shared_ptr 参数的构造函数。

  waek_ptr不提供直接通过operator*和operator->直接访问目标对象的能力,它必须提升为shared_ptr,再去访问目标对象。这样做是为了两点:

  1、从weak_ptr创建一个shared_ptr出来,可以检查weak_ptr是否还关联着一个目标对象。如果没有,那么视所用的操作而定,会抛出异常,或创建一个空的shared_ptr对象。

  2、在使用被引用的目标对象的时候,它一直存在。

  检测 weak_ptr 所引用的对象是否无效有几种方法:

  1、调用 expired(), 如果返回 true 表示不再持有目标对象了; 这等价于判断 use_count() == 0,但是前者会更快。

  2、用 weak_ptr 去构造一个 shared_ptr, 如果失败将抛出一个 bad_weak_ptr 异常——它是std::exception的派生类。

  3、使用 use_count() 去判断目标对象被引用的次数,但是它只能用于调试的目的,因为C++标准明确陈述过:use_count() 不必很高效。

  例如:

try {
  shared_ptr<std::string>  sp1{new string("ABCD")};
  weak_pr<std::string> wp(sp1);
  sp1.reset(); // sp1的目标对象被释放;
  wp.use_count(); // 返回0;
  wp.expired(); // 返回 true;
  shared_ptr sp2(wp); // 抛出std::bad_weak_ptr
}
catch(const std::exception& e) {
  std::cout << e.what() << std::endl; // 打印: "bad_weak_ptr".
}

三、shared_ptr的误用

  • shared_ptr的问题之一,就是引入 weak_ptr 的原因:循环引用会引入内存泄露;
  • shared_ptr的第二个注意点是必须保证一个对象只能被一个shared_ptr组所持有。只有通过shared_ptr之间的拷贝、赋值产生的shared_ptr才属于一个组,下面的两个shared_ptr在两个组:
    int *p = new int;
    shared_ptr<int> sp1(p);
    shared_ptr<int> sp2(p); // 错误!两个shared_ptr独立管理p的生命周期。

    这是错误的,因为在这里,p会被释放两次!当其中一个已经释放,那么另一个就成为了悬垂指针:它们互相并不知道对方的存在。正确的做法是让它们在同一个组:

    int *p = new int;
    shared_ptr<int> sp1(p);
    shared_ptr<int> sp2(sp1);

    只要不是未被任何 shared_ptr 管理的裸指针,都有类似的风险,例如:

    class X
    {
    public:
      void foo();
    private:
      std::shared_ptr<X> m_spSelf;
    };
    
    void X::foo()
    {
        m_spSelf = shared_ptr<X>(this); // Error! 新独立出一个组!
    }
    
    int main()
    {
       shared_ptr<X> spObj(new X);
       spObj->foo(); // Error! m_spSelf与spObj是两个组。
       return 0;
    }

    解决策略是:使用 enable_shared_from_this 模板类。

    class X : public enable_shared_from_this<X>
    {
    public:
      void foo();
    private:
      std::shared_ptr<X> m_spSelf;
    };
    
    void X::foo()
    {
        m_spSelf = shared_from_this();
    }
    
    int main()
    {
       shared_ptr<X> spObj(new X);
       spObj->foo(); // OK! m_spSelf与spObj在同一个组。
    }
  • <补充下原理:VS的enable_shared_from_this实现使用了模板元编程技巧。它通过一个成员typedef,typedef _EStype 作为开关,在shared_ptr构造时,构造函数内的_Enable_shared调用分支到了执行相应功能的那个版本。enable_shared_from_this 内部还持有一个私有的weak_ptr, _Wptr。_Enale_shared 在对象定义了enable_shared_from_this 时,会用构造产生的 shared_ptr 初始化它持有的_Wptr,后续调用 shared_from_this() 得到的都是从这个 _Wptr 提升来的 shared_ptr,所以和最初的shared_ptr在同一个组。做一个破坏性的小实验:定义一个空类,只包含public的 typedef selftype _EStype; 如果定义这个类的一个shared_ptr对象,在VS2010中就会崩溃~~>

四、shared_ptr 和 weak_ptr 的细节:

  • template<class T> shared_ptr { typedef T element_type; ...} element_type 可以是void,此时的shared_ptr代理一个 void*; 
  • 所有智能指针xp,如果它的所有权转移到了一个shared_ptr对象sp上,那么当sp要销毁对象时,会使用 xp 的 deleter。所有权转移可以是通过 operator=,或者 reset 完成的。注意:deleter 绝对不能抛出异常!(因为调用deleter时,shared_ptr 析构过程尚未结束,有可能导致内存泄露。)
  • shared_ptr可以用在不同类型的指针之间,只要它们可以隐式转换。因此,构造函数,拷贝构造,赋值运算符和 reset 函数都是成员模板
  • 未涉及的一些shared_ptr的方法:shared_ptr<T> sp(nullptr); shared_ptr<T> sp(nullptr, del); shared_ptr<T> sp(move(sp2)); shared_ptr<T> sp(sp2, ptr); // 别名构造器,与sp2共享对象,但是指向ptr; shared_ptr<T> sp(wp); 用weak_ptr构造shared_ptr; shared_ptr<T> sp(move(up)); // 从up和ap 转换为sp, 必须使用move语义,放弃所有权。sp = sp2; sp = move(up); sp = move(ap); sp1.swap(sp2); // 交换sp1和sp2的指针和deleter; swap(sp1, sp2) // 交换sp1和sp2的指针和deleter; sp.reset(); sp.reset(ptr, del); sp.reset(ptr, del, ac); make_shared<T>(...) // 创建一个shared_ptr; new和赋值的合并。

posted on 2014-01-22 20:50  SirDigit  阅读(995)  评论(0)    收藏  举报