utilities库——The C++ standard library, 2nd Edition 笔记(三)
目 录
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 给出tupletype第idx个元素的类型。即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;
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和赋值的合并。
浙公网安备 33010602011771号