【C++标准库】:第三章:通用工具

本文详细介绍了C++标准库中的通用工具,包括Pair和Tuple、Smart Pointer、数值极值、Type Trait和Type Utility等内容。重点讲解了Pair的构造、访问和比较,以及Smart Pointer的共享和独占拥有权概念,特别是shared_ptr和unique_ptr的使用和注意事项。此外,还提及了编译期分数运算和时间相关工具。
摘要由CSDN通过智能技术生成

【C++标准库】:第三章:通用工具

标签(空格分隔):【C++标准库】


第三章:通用工具

本章节主要内容为C++标准库的通用工具,包括:1. pair<> 和 tuple<>; 2. smart pointer class( chared_ptr<> 和 unique_ptr); 3. 数值极值(Numeric limit); 4. type tarit 和 type utility; 5. 辅助函数(例如:min(), max());6. class ratio<>; 7. clock 和 time;8. 若干重要的C函数。

3.1 Pair 和 Tuple

C++98 中提供了一个简单的class,用来处理类型不同的两个值,而不需要为它们再定义特别的class. 当标准函数需要返回一个value pair时码或者当容器元素是一个key/value pair时,就会用到该class.
TR1 引入了一个tuple calss, 它延伸了上述概念,接受任意(但有限)个元素。标准库实现保证可移植的范围是:允许tuple带有最多10个类型不同的元素。
C++11, tuple class 被重新实现,采用variadic template概念,此时可以用于任意大小的异质集合(heterogeneous collection). 但此时,calss pair依旧为两个元素服务。

3.1.1 Pair

Class pair 可以将两个 value 视为一个单元。C++标准库内多处用到了这个class, 尤其是容器map, multimap, unordered_map, unordered_multimap. 任何函数如果需要返回两个value,也需要使用pair.
Struct pair 定义于 < utility>, 提供下列各项操作(见下)。原则上可以对pair<> 执行creat, copy/assign/swap 及 compare 操作,此外还提供first_type 和 second_type类型定义式,用来表示第一value和第二value的类型。

  • 元素访问:为了让程序能够处理pair的两个值,它提供了“直接访问对应数据成员”的能力。
    事实上,由于它是个struct而不是calss,以至于所有成员都是public:
namespace std{
    template<typename T1, typename T2>
    struct pair{
        //member
        T1 first;
        T2 second;
    };
}
操作函数 影响
pair< T1, T2> p Default 构造函数,建立一个pair,元素类型分别为T1,T2,各自以其defalut构造函数初始化
pair< T1, T2> p(val,val) 建立一个pair,元素类型分别为T1,T2,以val和val为初值
pair< T1, T2> p(rv1, rv2) 建立一个pair,元素类型分别为T1,T2,以rv1和rv2进行搬移式初始化(move initialized)
pair< T1, T2> p(piecewise_construct,t1,t2) 建立一个pair,元素类型分别为 tuple T1,T2,以tuple T1和 T2 的元素为初值
pair< T1, T2> p(p2) Copy构造函数,建立p成为p2的拷贝
pair< T1, T2> p(rv) Move构造函数,将rv的内容移至p中(允许隐式类型转换)
p = p2 将p2赋值给p
p = pv 将rv的值move assign给p
p.first 直接成员访问,获得pair内第一个值
p.second 直接成员访问,获得pair内第二个值
get< 0>§ C++11后,等价于p.first
get< 1>§ C++11后,等价于p.second
p1 == p2 等价于 p1.first == p2.first && p1.second == p2.second
p1 != p2 等价于 !(p1==p2)
p1 < p2
p1 > p2
p1 <= p2
p1 >= p2
p1.swap(p2) C++11后,互相交换p1和p2的数据
swap(p1,p2) C++11后,互相交换p1和p2的数据
make_pair(val1,val2) 返回一个pair,带有val1, val2的类型和数值

例如:

pair<int, double> p(4, 3.14);
cout<<get<0>(p)<<endl;      //输出 4
cout<<get<1>(p)<<endl;      //输出 3.14
cout<<tuple_size< pair<int, double>>::value<<endl;//输出 2
  • 构造函数和赋值:
    default构造函数生成一个pair时,以两个“被default构造函数个别初始化”的元素作为初值。
    例如:
pair<int, float> p; //iniuutialize p.first and p.second with zero.
//就是以int()和float()来初始化p

copy 构造函数同时存在两个版本:版本1接受相同类型的pair;版本2是一个member template, 在“构造过程中需要隐式类型转换”是被调用。如果pair对象被复制,调用的是被隐式合成的那个copy构造函数。

  • 逐块式构造(Piecewise Construction): pair<>提供三个构造函数,用来初始化first和second成员.
namespace std{
    template <typename T1, typename T2>
    struct pair{
        pair(const T1& x,const T2& y); //方法一
        template <typename U, typename V> pair(U&&& x, V&& y);     //方法二
        //方法三
        template< typename ... Args1, typename ... Args2> pair(piecewise_construct_t, tupe<Args1...> first_args, tuple<Args2...> second_args);
    };
}
  • 便携函数make_pair(): template函数make_pair() 使得程序员无须写出类型就能生成对象。
    例如,以下两种形式是等价的。
pair<int, double>(4, 3.14);
make_pair(4, 3.14);
//自C++11后,也可以使用初值列(initializer list);
{4, 3.14}
  • pair之间的比较:两个pair对象内的所有元素相等,这两个pair对象才被视为相等(equal).
namespace std{
    template<typename T1, typename T2>
    bool operator== (const pair<T1, T2>& x, const pair<T1, T2>& y){
    return x.first == y.first && x.second == y.second;
    }
}
//两个pair互相比较时,first具有较高的优先级,如果第一个元素不相等,则其比较结果直接返回。如果first相等,才继续比较second.

其他的比较操作符(comparison operator)如法炮制。

  • pair运用实例:C++标准库中大量使用pair. 例如 (unordered) map 和 multimap容器的元素类型便是pair, 也就是一对key/value; 另外,C++中凡事“必须返回两个value”的函数一般也使用pair对象。

3.1.2 tuple(不定数的值组)

tuple 是 TR1 引入的东西,它扩展了 pair 的概念,拥有任意数量的元素。也就是说,tuple呈现出一个异质元素列(heterogeneous list of elements), 其中每个类型都可以被指定,或者由编译期推导。
然而,由于 TR1使用C++98的语言特性,因此不能定义出一个“参数个数不定”的template,基于这个原因,实现必须具体指明“一个tuple可以拥有的”所有可能的元素个数。TR1对此的建议是至少10个实参,这意味这tuple往往被如下定义:

//T0~T9至少10个类型各异的template参数,每个都带有实现赋予的默认类型。
template<typename T0 =..., ......, typename T9 = ...>
class tuple

自C++11以来,variadic template 被引入,使template可以接受任何数量的template实参,于是< tuple> 中的calss tuple 声明式就被简化为如下:

namespace std{
    template Mtypename ...Types>
    class tuple;
}
  • tuple的操作:原则上,tuple的接受十分直观:1. 通过明白的声明,使用便捷函数make_tuple(), 可以创建一个tuple. 2. 通过get<>() function template, 可以访问tuple的元素。
    例如:
//创造一个异质的四元素tuple, 每个元素的内容由default构造函数初始化。
    tuple<string, int , int, double> t1;
    
    //显式创造并初始化一个异质的三元素tuple
    tuple<int, float, string> t2( 41, 6.3, "test");
    cout<<get<0>(t2)<<" ";//输出为41;
    cout<<get<1>(t2)<<" ";//输出为6.3;
    cout<<get<2>(t2)<<" ";//输出为test;
    
    //使用make_tuple创建一个tuple
    auto t3 = make_tuple(22,44,"test2");
    
    //比较和赋值
    if( t2 < t3)
        t2 = t3;    //It's Ok,
    
    //tuple的元素类型可以是reference, 例如;
    string s = "test3";
    tuple<string&> t4(s);
    cout<<get<0>(t4)<<" ";//输出为test3
    get<0>(t4) = "modify";
    cout<<get<0>(t4)<<" ";//输出为modify
    
    //tuple不是寻常容器,不允许迭代元素,因此必须在编译期知道待处理元素的索引值
    int i; get<i>(t2);// compile-time ERROR: i is no compile-time value;
    
操作函数 影响
tuple< T1, T2,…,Tn> t 以n个给定类型的元素建立一个tuple, 每个元素的内容由default构造函数初始化
tuple< T1, T2,…,Tn> t(v1,v2,…,vn) 以n个给定类型的元素建立一个tuple, 以给定值进行初始化
tuple< T1, T2> t§ 建立一个tuple, 带有两个元素,分别使用给定的类型,并以给定的pair p为初值(类型必须吻合)
t = t2 将 t2 赋值给 t
t = p 并以给定的pair p为初值(类型必须吻合)
t1 == t2 如果所有元素比较相则返回true, 否则返回false
t1 != t2
t1 > t2
t1 < t2
t1 >= t2
t1 <= t2
t1.swap( t2) 自C++11后,互换t1, t2数据
swap(t1,t2) 同上
make_tuple(v1,v2,…) 以传入的所有数值和类型建立一个tuple,并允许由此tuple, 并允许由此tuple提供数值
tie( ref1, ref2,…) 建立一个由reference构成的tuple, 并允许由此tuple提取个别数值
  • 便捷函数make_tuple() 和 tie()
    便捷函数make_tuple() 会依据value建立tuple,不需要明确指明元素的类型。可见上例。
    借由特比的函数对象reference_wrapper<> 及便捷函数ref() 和 cref() (自C++11起并定义于),可以影响make_tuple()产生的类型,也可以改变tuple内的值。

    如果想最方便地在tuple中使用reference, 可以选择tie(), 它可以建立一个内含有reference的tuple:

tuple< int, float, string> t(77, 1.1, "test");
int i; float f; string s;
tie(i,f,s) = t;//以i,f,s的reference建立一个tuple,因此该赋值操作就是将t内的元素分别给i,f,s
cout<<i<<" "<<f<<" "<<s; //输出 77 1.1 test
  • tuple 和 初值列(initializer list)
    各个构造函数中,“接受不定个数的实参”的版本被声明为explicit:
namespace std{
    template <typename ...Types>
    class tuple{
        public:
        explicit tuple( const Types& ...);
        template<typename ...UTypes> explicit tuple(UTypes&&...)
        }
    }
    
//例:
tuple<int, double> t1(42, 3.14); //Ok, old syntax
tuple<int, double> t2{42, 3.14}; //Ok, new syntax
tuple<int, double> t3 = {42, 3.14}; //ERROR
vector< tuple<int,float>> v{
  {1,1.0},{2,2.0}};//ERROR
tuple<int, int> foo(){
    return [1,2};   //ERROR
}
vector< tuple<int,float>> v{make_tuple(1,1.0)};//OK
tuple<int, int> foo(){
    return {1,2};   //OK
}
  • 其他的tuple特性:
  1. tuple_size< tupletype>::value 获得元素个数.
  2. tuple_element< idx, tupletype>::type 获得第idx个元素的类型.
  3. tuple_cat() 将多个tuple串接成一个tuple
    例如:
tuple<int, float, string > test;
tuple_size<test>::vaule;        //输出3
tuple_element<1,test>::type;    //float
  • tuple的输入与输出
    tuple class最初公开与Boost程序库,在其中,tuple可以将其元素写到output stream, 但是在C++标准库并不支持这项操作,但如果拥有util/printtuple.hpp,就可以使用操作符<<来打印tuple.

  • tuple与pair的转化:可以用pair作为初值来初始化一个双元素tuple; 也可以将一个pair赋值给一个双元素tuple.

3.2 Smart Pointer( 智能指针)

pointer 十分重要,但也会带来麻烦。pointer可以在作用域边界之外拥有reference的语义。但是,维持pointer寿命及所指向对象的寿命却十分棘手。
解决方法:使用smart pointer. 称为smart的原因是该指针可以知道自己是否是指向某个对象的嘴个一个指针,并运用该点来决定在适当的时刻是否销毁自身。
C++11起,C++标准库提供了两大类型的smart pointer: 1. class shared_ptr 实现的共享式拥有(shared ownership) 的概念。多个smart pointer可以指向相同的对象,该对象和其相关资源会在“最后一个reference被销毁”时释放。为了在结构比较复杂的情景中执行上述工作,标准库提供了 weak_ptr, bad_weak_ptr 和 enable_shared_from_this 等辅助类。2. class unique_ptr 实现独占式拥有(exclusive ownership) 或称为 严格拥有(strict ownership) 概念,保证同一时间只有一个smart pointer 可以指向对象,可以交接拥有权,可以避免资源泄漏。

3.2.1 class shared_ptr

几乎每个程序都需要具有“在相同时间的多处地点处理或使用对象”的能力。为此,必须在程序的多个地点指向同一个对象,虽然C++提供了reference, 但为了确保“指向对象”的最后一个reference被删除时对象本身也被删除,所以需要“当对象不再被使用时就被清理”的语义。
class shared_ptr 提供了这样的共享式拥有语义,即多个 shared_ptr 可以拥有同一个对象,对象的最末一个有责任销毁对象,并清理与该对象相关的所有资源。
如果对象以new产生,则默认情况下清理工作由delete完成,但也可以定义其他的清理方法。

  • 使用 shared_ptr:可以像任何其他pointer一样使用 shared_ptr, 可以赋值、拷贝、比较它们,也可以使用操作符 * 或 -> 来访问其所指向的对象。
    例如:
//声明两个shared_ptr并各自指向string
    shared_ptr<string> pNico( new string("nico"));
    shared_ptr<string> pJutta( new string("jutta"));
    //注意,由于“接受单一pointer作为唯一实参“的构造函数是explicit,所以形如以下
    //shared_ptr<string> pNico =  new string("nico"); //ERROR
    
    //就像使用寻常pointer的用法
    (*pNico)[0] = 'N';          //运用operator *, 取得pNico指向的对象
    pJutta->replace(0, 1, "J"); //运用 operator ->, 取得pJutta指向的对象内第一个成员
    
    //多次安插以上两个pointer.
    //容器为c传入的元素创造属于容器自己的拷贝
    vector< shared_ptr<string>> whoMadeCoffee;
    whoMadeCoffee.push_back(pJutta);
    whoMadeCoffee.push_back(pJutta);
    whoMadeCoffee.push_back(pNico);
    whoMadeCoffee.push_back(pJutta);
    whoMadeCoffee.push_back(pNico);
    
    for( auto ptr:whoMadeCoffee)
        cout<< *ptr<<endl;//按插入顺序输出
    
    *pNico = "Nicolai"; //该对象所有身影都转换为新的值"Nicolai".
    
    //针对vector内第一个shared pointer 调用use_count(), 输出该shared pointer所指向对象当前拥有着的数量
    //vector中第一个元素所指向的对象有四个主人:vector中有三个,pJutta本身一个。
    cout<<endl<<"use_count = "<<whoMadeCoffee[0].use_count()<<endl;//输出use_count = 4
  • 定义一个delete:可以声明属于自己的delete.
    例如:
//例如:让它在“删除被指向对象”之前先打印一条信息
shared_ptr<string> pNico( new string("nico"), [](string* p){
                                                    cout<<" delete "<<*p<<endl;
                                                    delete p;
                                                }
                         );
shared_ptr<string> pNico( new string("nico"));
pNico = nullptr;//此会输出一个 delete nico.
  • 对付array: shared_ptr 提供的 default deleter 调用的是 delete, 而不是 delete[], 这意味着只有当shared pointer拥有"有new建立起来的但一对象", default deleter 才能适才适所。
    很不幸,为array建立一个 shared_ptr 是可能的,但是确是错误的:
shared_ptr<int> p( new int[10]); //ERROR, but compiles

因此,如果使用new[]来建立一个array of object, 必须定义自己的deleter.
例如:

//传递一个函数、函数对象或lambda, 让它们针对传入的寻常指针调用delete[].
shared_ptr<int> p( new int[10], [](int* p){
                                    delete[] p;
                                }
                );

3.2.2 class week_ptr

class weak_ptr 允许“共享但不拥有”某对象。该class会建立起一个shared pointer, 一旦最后一个拥有该对象的shared pointer 失去了拥有权,任何 weak pointer 都会自动成空。
因此,除了defalut和copy构造函数之外,class weak_ptr 只能提供“接受一个shared_ptr”的构造函数。
weak_ptr指向的对象,不能使用操作符* 和 -> 访问,而是必须建立一个shared_ptr,这是因为:1. 在weak pointer 之外建立一个shared pointer 可因此检查是否存在一个相应对象。2. 当指向的对象正被处理时,shared pointer 无法被释放
因此,class weak_ptr 只能提供小量操作,只能够用来创建、赋值、赋值 weak pointer, 以及转换为一个shared pointer, 或检查自己是否指向某个对象。

  • 使用 week_ptr:
//当使用shared_ptr时
class Person{
    //拥有一个name和若干reference指向其他Person
public:
    string name;
    shared_ptr<Person> mother;
    shared_ptr<Person> father;
    vector<shared_ptr<Person>> kids;
    
    Person( const string& n, shared_ptr<Person> m = nullptr, shared_ptr<Person> f = nullptr):name(n), mother(m), father(f){
        
    }
    
    ~Person(){
        cout<<" delete "<<name<<endl;
    }
};

shared_ptr<Person> initFamily( const string& name){
    //建立三个Person: mom dad kid, 根据传入实参将所有姓名初始化
    shared_ptr<Person> mom( new Person( name + "'s mom"));
    shared_ptr<Person> dad( new Person( name + "'s dad"));
    shared_ptr<Person> kid( new Person( name, mom, dad));
    //设定kid的父母,并将kid插入到对应父母的kids容器内部
    mom->kids.push_back(kid);
    dad->kids.push_back(kid);
    //返回kid
    return kid;
}
/*-----------------------------*/
int main() {
    shared_ptr<Person> p = initFamily("nico");
    //p是指向上述家庭的最后一个handle. 在内部,每个Person对象都有reference从kid指向其父母以及反向指向。
    cout<<" nico's family exists"<<endl;
    cout<<" - nico is shared "<<p.use_count()<<" times "<<endl;
    cout<<" - name of 1st kid of nicos mom: "<<p->mother->kids[0]->name<<endl;
    
    p = initFamily("jim");
    cout<<" jim's family exists "<<endl;
    //输出
    //nico's family exists
    //- nico is shared 3 times
    //- name of 1st kid of nicos mom: nico
    //jim's family exists
return 0;}


//使用weak_ptr时, 注意 vector中的指针变化
class Person{
public:
    string name;
    shared_ptr<Person> mother;
    shared_ptr<Person> father;
    vector<weak_ptr<Person>> kids; // weak_ptr
    
    Person( const string& n, shared_ptr<Person> m = nullptr, shared_ptr<Person> f = nullptr):name(n), mother(m), father(f){
        
    }
    
    ~Person(){
        cout<<" delete "<<name<<endl;
    }
};
//main函数中第四行改为 “cout<<" - name of 1st kid of nicos mom: "<<p->mother->kids[0].lock()->name<<endl;”
//执行main,得到:
// nico's family exists
// - nico is shared 1 times 
// - name of 1st kid of nicos mom: nico
// delete nico
// delete nico's dad
// delete nico's mom
// jim's family exists 
// delete jim
// delete jim's dad
// delete jim's mom
  • 误用shared pointer
    虽然shared_ptr 强化了程序安全,但是由于对象的相应资源往往被自动释放,当对象不再被使用时可能出现问题。例如:由于循环依赖(cyclic dependency) 造成"dangling pointer"(空荡指针)。
    另外,必须保证某个对象只能被一组shared pinter拥有。例如一下代码是错误的:
int* p = new int;
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);
//ERROR, 当sp1 和 sp2 都会在丢失p的拥有权时候调用delete. 这意味着相应的资源会被释放两次

3.2.4 细究 shared pointer 和 weak pointer

  • 细究 shared pointer : class shared_ptr 提供的是带有“共享式拥有”语义的smart pointer概念,无论何时当shared pointer 的最后一个拥有着被销毁,相应对象就会被delete.
    class shared_ptr<> 被模版化,模版参数是“原始pointer所指对象”的类型:
namespace std {
    template <typename T>
    class shared_ptr{
    public:
        typedef T element_type;
        //...
    };
}
//元素类型可以是void, 意味着 shared pointer 共享的对象又一个未具体说明的类型,例如void* .

一个empty shared_ptr 并不能分享对象拥有权,所以 use_count() 返回0. 然而,由于设计了一个特殊构造函数,使得empty shared pointer 还可以指向对象。
下表给出shared pointer 提供的所有操作。

操作 效果
shared_ptr< T> sp default构造函数,建立一个empty shared pointer, 使用 default deleter(即delete)
shared_ptr< T> sp(ptr) 建立一个shared pointer, 令其拥有 *ptr, 使用 default deleter(即delete)
shared_ptr< T> sp( ptr, del) 建立一个shared pointer, 令其拥有 *ptr, 使用del作为 deleter
shared_ptr< T> sp( ptr, del, ac) 建立一个shared pointer, 令其拥有 *ptr, 使用del作为 deleter并以ac为allocator
shared_ptr< T> sp(nullptr) 建立一个empty shared pointer, 使用 default deleter(即delete)
shared_ptr< T> sp(nullptr, del) 建立一个empty shared pointer, 使用del作为 deleter
shared_ptr< T> sp(nullptr, del, ac) 建立一个empty shared pointer, 使用del作为 deleter并以ac为allocator
shared_ptr< T> sp(sp2) 建立一个shared pointer, 与sp2共享拥有权
shared_ptr< T> sp(move(sp2)) 建立一个shared pointer, 拥有sp2先前拥有的pointer, 之后将sp2改为empty
shared_ptr< T> sp(sp2, ptr) Alias构造函数,建立一个shared pointer, 与sp2共享拥有权,但指向*ptr
shared_ptr< T> sp(wp) 基于一个weak pointer 创建出一个shared pointer
shared_ptr< T> sp(move(up)) 基于一个unique_ptr up 创建出一个shared pointer
shared_ptr< T> sp(move(ap)) 基于一个auto_ptr up 创建出一个shared pointer
sp.~shared_ptr() 析构函数,调用delete——如果sp拥有一个对象
sp = sp2 赋值,sp共享sp2的拥有权,放弃之前对象的拥有权
sp = move(sp2) move assignment(sp2将拥有权移交给sp)
sp = move(up) 赋予一个unique_ptr up(up将
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值