摘自http://www.wuzesheng.com/?p=1972
参考http://www.ibm.com/developerworks/cn/aix/library/au-gcc/
- 1. auto用来声明变量
大家先看看下面的代码,一个我们比较熟悉的C++98中遍历map的例子:
std::map< std::string, std::vector<int> > mp; for (std::map< std::string, std::vector<int> >::iterator i = mp.begin(); i != mp.end(); ++i) { /// traverse the map }
下面再来看一个C++0x改进版的同样的例子:std::map< std::string, std::vector<int> > mp; for (auto i = mp.begin(); i != mp.end(); ++i) { /// traverse the map }
通过对比,不难发现,用了auto之后,整个代码简洁了很多。其实auto可以用在很多地方,尤其是在复杂的多层嵌套模板中,用auto可以使代码简化不少,这个也是在这里选用map做例子的重要原因。另外auto前后还可以加各种修饰符,看下面的例子:
float data[BufSize]; /// data: float[BufSize] auto v3 = data; /// v3: float* auto& v4 = data; /// v4: float (&)[BufSize] auto x1 = 10; /// x1: int const auto *x2 = &x1; /// x2: const int*
- 2. “>>”可以做为嵌套模板的闭合端,不需要显式的加空格
用过C++模板的朋友应该比较清楚,在C++98中,嵌套模板的闭合的多个”>”之间必须显式的加上空格,要不然编译会不通过,因为C++98的标准认为”>>”这个是右移运算符,用在模板中是非法的。值得大家高兴的是,C++0x已经把这个蹩脚的限制给取消了,以后”>>”也可以用在嵌套模板的闭合端了,没有限制了,请看下面的例子:
std::vector< std::list<int>> vi1; /// fine in C++0x, error in C++98 std::vector< std::list<int> > vi2; /// fine in C++0x and C++98
3. 基于范围的loop -
在具体介绍这个特性之前,我们首先来看一下,在C++98中,要遍历一个标准的容器是如何做的,这里以vector为例:
std::vector< int> v; for (std::vector< int>::iterator i = v.begin(); i != v.end(); ++i) { /// use iterator i to operate the element in v }
下面再来看一下C++0x的改良版:
std::vector< int> v; for (int num: v) /// for (auto num: v) is also OK { /// num is the element in v } std::vector< int> v; for (int & num: v) /// for (auto & num: v) is also OK { /// num is the reference of the element in v }
对比一下,上面的代码比之前的简洁了很多,对于写程序的人来说,能通过简洁的方式实现,决不会用冗长的方式,一方面简洁的方式开发效率要高,另一方面简洁的代码看着也赏心悦目。通过上面的例子,相信大家对基于范围的loop已经有了初步的认为,下面来介绍一下,什么样情况下可以使用基于范围的loop:
(1)首先,重中之重,基于范围的loop只能用在for loop中,不能用于while, do while的loop
(2)所有的C++0x中的容器(当然包括C++98中的容器)都可以使用
(3)可以用在初始化列表和正则表达式的匹配结果(这两个都是C++0x新特性,后面会介绍)
(4)用户自己定义的类型,如果支持begin(),end()及迭代器遍历也可以用 - 4. 新增加空指针类型nullptr
我们知道,在C++98中,空指针用NULL表示,而NULL只是define的一个值为0或0L的宏。在大多数情况下,NULL是没有问题的,但是在模板的参数自动推导中,NULL会被推导为int类型,这样就不能匹配指针类型了,请看下面的例子:
template< typename F, typename P > void DoAndCall(F func, P param) { /// do something func(param); } void Func(int * ptr); Func(0); /// ok Func(NULL); /// ok Func(nullptr); /// ok DoAndCall(Func, 0); /// error DoAndCall(Func, NULL); /// error DoAndCall(Func, nullptr); /// ok
上面的例子中,传入NULL的时候,NULL被推导为int类型,而Func需要的是一个int *类型的参数,这时候编译就会有问题。也正是因为NULL的这个缺陷,C++0x引入了nullptr这个新的关键字,nullptr的类型是std::nullptr_t,它既具有原来NULL做为空指针的所有特性,还很好的解决了上面提到的问题。 - 5. unicode的支持,新增加char16_t和char32_t类型
c++98支持char和wchar两种字符类型,c++0x为了更好的支持unicode,新增了char16_t和char32_t类型,关于char16_t和char32_t的用法,和char/wchar很类似,看下面的例子:
'x' /// char 'x' L'x' /// wchar_t 'x' u'x' /// char16_t 'x', UCS-2 U'X' /// char32_t 'x' UCS-4 /// std::basic_string typedefs for all character types: std::string s1; /// std::basic_string< char> std::wstring s2; /// std::basic_string< wchar_t> std::u16string s3; /// std::basic_string< char16_t> std::u32string s4; /// std::basic_string< char32_t>
6 原字符串的支持(raw string) -
在C++98中,有控制字符和转义字符的概念,像’\n’用来回车等,但有的时候,我们就想把像控制字符这样的特殊字符原样的传输出,不做特殊处理,这时候就需要转义,显得比较麻烦,尤其在正则表达式中,格外突出。C++0x增加了raw string这个概念,也叫原字符串,这样的字符串没有特殊字符,所有的字符都是普通字符,请看下面的例子:
std::string noNewlines(R"(\n\n)"); std::string cmd(R"(ls /home/docs | grep ".pdf")");
在上面的例子中,我们看到raw string的语法是 R”(…)”,但是有时候,在正则表达式中,”)会做为匹配的特征,这时候该怎么办呢?C++0x也考虑了这一点,raw string的分隔符可以灵活指定,看下面的例子:std::regex re1(R"!("operator\(\)"|"operator->")!"); /// "operator()"| "operator->" std::regex re2(R"xyzzy("\([A-Za-z_]\w*\)")xyzzy"); /// "(identifier)"
上面的例子中,第一个例子,用”!(…)!”做了分隔,第二个例子中,用”xyzzy(…)xyzzy”做了分隔,都是因为正规表达式本身就有)”。用户可以指定各种各样的分隔符,原则就是不跟raw string text中的字符不相冲突即可。
我们知道变量的初始化在C++中是一个比较重要的话题,好的编程习惯建议大家在声明变量的时候最好初始化,这样可以避免一些不必要的问题。另外,在C++98中,变量的初始化(initialization)和赋值(assignment)是两个比较重要的概念,这里大家可以回顾一下,比如常量只能初始化,不能赋值。
今天要讲的主要内容就是C++0x中新增加的统一初始化的语法,通过与C++98中的对比,我们来加深对C++0x的理解。下面我们来看一些C++98中初始化的例子:
/// 各种形式的初始化 const int x(5); ///< 直接初始化 const int y = 5; ///< copy构造初始化 int arr[] = {1, 2, 3}; ///< 大括号初始化 struct Point{int x, y;}; const Point p = {1, 2}; ///< 大括号初始化 class PointX { public: PointX(int x, int y); private: int x, y; }; const PointX px(1, 2); ///< 构造函数初始化 std::vector< int> vec(arr, arr+3); ///< 从别的容器初始化
从上面的例子,我们可以看到,C++98支持各种各样的初始化方式,另外,还有一些在C++98中不能直接初始化的情况,如下所示:/// 不能初始化的情况 class Foo { public: Foo() : data(???) {} ///< 不能在这里初始化数组 private: int data[3]; }; int * data = new int[3]; ///< 不能在这里初始化堆上内存
上面的例子,相信有C++基础的朋友一看就会懂,我们看到,在C++98中初始化是这样的复杂,有多种的形式,还有不能初始化的,实在是太麻烦了。正是基于这样的情况,C++0x提出统一了初始化的语法,并且支持所有类型的初始化,彻底把C++程序员从这种复杂的逻辑中解放出来。C++0x中,初始化都可以通过大括号初始化来完成,可以用在各种地方,下面先看例子:/// 与上面c++98中能初始化的例子相对应 int x {1}; int y {2}; int arr[] {x, y, 3}; struct Point {int x, y;}; Point p {1, 2}; class PointX { public: PointX(int x, int y); private: int x, y; }; PointX px {1, 2}; std::vector< int> vec {1, 2, 3}; /// 与上面C++98中不能初始化的例子相对应 class Foo { public: Foo() : data {1,2,3} {} private: int data[3]; }; int * data = new int[3] {1, 2, 3};
从上面的例子中,我们可以看到,C++0x中,所有的初始化都可以用{}来完成了,达到了真正意义上的统一,语法也十分简洁。另外,原来在C++98不能直接初始化的情况,C++0x也顺带解决了。不急,还有更炫的地方,先看下面的例子:
void Func(const std::vector< int>& v); Func({1, 2, 3, 4, 5}); Point MakePoint() { return { 0, 0 }; }
看到这里,不知道大家有没一种觉得很炫的感觉,我个人是觉得,C++0x已经把这个统一初始化做到了极致,既统一了形式,又支持了很多方便的用法,简洁、实用。上面举了不少例子,相信大家对C++0x中统一初始化的语法已经有了比较深刻的认识,这里还有一些需要补充的点,帮助大家更好的理解C++0x中的初始化:
1. 在C++中,有集合类型(Aggregates)和非集合类型(Non-Aggregates)之分,集合类型包括数组和用户没有提供构造函数、没有protected/private的非静态成员、没有基类、没有虚函数的class类型。其它都属于非集合类型。对于这两种类型,C++0x采用了不同的初始化的逻辑(虽然形式上一样):
(1)对于集合类型,采用从头到尾,每个元素逐一初始化的方式进行初始化
(2)对于非集合类型,调用该类型的构造函数进行初始化2. 在C++0x的的统一初始化逻辑中,参数的个数可以少于成员,但不能多于,如果少于的话,没初始化的成员通过缺省的方式初始化
struct Point {int x, y;}; Point p1 {1}; ///< ok Point p2 {1, 2, 3}; ///< error!
3. C++0x中增加了一种新的类型std::initializer_list, {}初始化列表可以转为std::initializer_list类型,这个类型也可以直接由{}来初始化
4. C++0x的{}初始化逻辑还支持=号,不过因为=号不是在所有的情况都适用,所以这里不建议在{}初始化的时候使用=,培养直接使用大括号初始化的习惯即可。
const int x = {1}; struct Point {int x, y;}; Point p = {1, 2};
C++0x中新增的lambda表达式, function对象和bind机制。之所以把这三块放在一起讲,是因为这三块之间有着非常密切的关系,通过对比学习,加深对这部分内容的理解。在开始之间,首先要讲一个概念,closure(闭包),这个概念是理解lambda的基础。下面我们来看看wikipedia上对于计算机领域的closure的定义:A closure (also lexical closure, function closure or function value) is a function together with a referencing environment for the non-local variables of that function.
上面的大义是说,closure是一个函数和它所引用的非本地变量的上下文环境的集合。从定义我们可以得知,closure可以访问在它定义范围之外的变量,也即上面提到的non-local vriables,这就大大增加了它的功力。关于closure的最重要的应用就是回调函数,这也是为什么这里把function, bind和lambda放在一起讲的主要原因,它们三者在使用回调函数的过程中各显神通。下面就为大家一步步接开这三者的神秘面纱。
- 1. function
我们知道,在C++中,可调用实体主要包括函数,函数指针,函数引用,可以隐式转换为函数指定的对象,或者实现了opetator()的对象(即C++98中的functor)。C++0x中,新增加了一个std::function对象,std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。我们来看几个关于function对象的例子:
#include < functional> std::function< size_t(const char*)> print_func; /// normal function -> std::function object size_t CPrint(const char*) { ... } print_func = CPrint; print_func("hello world"): /// functor -> std::function object class CxxPrint { public: size_t operator()(const char*) { ... } }; CxxPrint p; print_func = p; print_func("hello world");
在上面的例子中,我们把一个普通的函数和一个functor赋值给了一个std::function对象,然后我们通过该对象来调用。其它的C++中的可调用实体都可以像上面一样来使用。通过std::function的包裹,我们可以像传递普通的对象一样来传递可调用实体,这样就很好解决了类型安全的问题。了解了std::function的基本用法,下面我们来看一些使用过程中的注意事项:- (1)关于可调用实体转换为std::function对象需要遵守以下两条原则:
a. 转换后的std::function对象的参数能转换为可调用实体的参数
b. 可高用实体的返回值能转换为std::function对象的(这里注意一下,所有的可调用实体的返回值都与返回void的std::function对象的返回值兼容)。 - (2)std::function对象可以refer to满足(1)中条件的任意可调用实体
- (3)std::function object最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等
- (1)关于可调用实体转换为std::function对象需要遵守以下两条原则:
- 2. bind
bind是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。C++98中,有两个函数bind1st和bind2nd,它们分别可以用来绑定functor的第一个和第二个参数,它们都是只可以绑定一个参数。各种限制,使得bind1st和bind2nd的可用性大大降低。C++0x中,提供了std::bind,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定,这个bind才是真正意义上的绑定,有了它,bind1st和bind2nd就没啥用武之地了,因此C++0x中不推荐使用bind1st和bind2nd了,都是deprecated了。下面我们通过例子,来看看bind的用法:
#include < functional> int Func(int x, int y); auto bf1 = std::bind(Func, 10, std::placeholders::_1); bf1(20); ///< same as Func(10, 20) class A { public: int Func(int x, int y); }; A a; auto bf2 = std::bind(&A::Func, a, std::placeholders::_1, std::placeholders::_2); bf2(10, 20); ///< same as a.Func(10, 20) std::function< int(int)> bf3 = std::bind(&A::Func, a, std::placeholders::_1, 100); bf3(10); ///< same as a.Func(10, 100)
上面的例子中,bf1是把一个两个参数普通函数的第一个参数绑定为10,生成了一个新的一个参数的可调用实体体; bf2是把一个类成员函数绑定了类对象,生成了一个像普通函数一样的新的可调用实体; bf3是把类成员函数绑定了类对象和第二个参数,生成了一个新的std::function对象。看懂了上面的例子,下面我们来说说使用bind需要注意的一些事项:- (1)bind预先绑定的参数需要传具体的变量或值进去,对于预先绑定的参数,是pass-by-value的
- (2)对于不事先绑定的参数,需要传std::placeholders进去,从_1开始,依次递增。placeholder是pass-by-reference的
- (3)bind的返回值是可调用实体,可以直接赋给std::function对象
- (4)对于绑定的指针、引用类型的参数,使用者需要保证在可调用实体调用之前,这些参数是可用的
- (5)类的this可以通过对象或者指针来绑定
- 3. lambda
讲完了function和bind, 下面我们来看lambda。有python基础的朋友,相信对于lambda不会陌生。看到这里的朋友,请再回忆一下前面讲的closure的概念,lambda就是用来实现closure的东东。它的最大用途也是在回调函数,它和前面讲的function和bind有着千丝万缕的关系。下面我们先通过例子来看看lambda的庐山真面目:
vector< int> vec; /// 1. simple lambda auto it = std::find_if(vec.begin(), vec.end(), [](int i) { return i > 50; }); class A { public: bool operator(int i) const { return i > 50; } }; auto it = std::find_if(vec.begin(), vec.end(), A()); /// 2. lambda return syntax std::function< int(int)> square = [](int i) -> int { return i * i; } /// 3. lambda expr: capture of local variable { int min_val = 10; int max_val = 1000; auto it = std::find_if(vec.begin(), vec.end(), [=](int i) { return i > min_val && i < max_val; }); auto it = std::find_if(vec.begin(), vec.end(), [&](int i) { return i > min_val && i < max_val; }); auto it = std::find_if(vec.begin(), vec.end(), [=, &max_value](int i) { return i > min_val && i < max_val; }); } /// 4. lambda expr: capture of class member class A { public: void DoSomething(); private: std::vector<int> m_vec; int m_min_val; int m_max_va; }; /// 4.1 capture member by this void A::DoSomething() { auto it = std::find_if(m_vec.begin(), m_vec.end(), [this](int i){ return i > m_min_val && i < m_max_val; }); } /// 4.2 capture member by default pass-by-value void A::DoSomething() { auto it = std::find_if(m_vec.begin(), m_vec.end(), [=](int i){ return i > m_min_val && i < m_max_val; }); } /// 4.3 capture member by default pass-by-reference void A::DoSomething() { auto it = std::find_if(m_vec.begin(), m_vec.end(), [&](int i){ return i > m_min_val && i < m_max_val; }); }
上面的例子基本覆盖到了lambda表达的基本用法。我们一个个来分析每个例子(标号与上面代码注释中1,2,3,4一致):- (1)这是最简单的lambda表达式,可以认为用了lambda表达式的find_if和下面使用了functor的find_if是等价的
- (2)这个是有返回值的lambda表达式,返回值的语法如上面所示,通过->写在参数列表的括号后面。返回值在下面的情况下是可以省略的:
a. 返回值是void的时候
b. lambda表达式的body中有return expr,且expr的类型与返回值的一样 - (3)这个是lambda表达式capture本地局部变量的例子,这里三个小例子,分别是capture时不同的语法,第一个小例子中=表示capture的变量pass-by-value, 第二个小拿出中&表示capture的变量pass-by-reference,第三个小例子是说指定了default的pass-by-value, 但是max_value这个单独pass-by-reference
- (4)这个是lambda表达式capture类成员变量的例子,这里也有三个小例子。第一个小例子是通过this指针来capture成员变量,第二、三个是通过缺省的方式,只不过第二个是通过pass-by-value的方式,第三个是通过pass-by-reference的
分析完了上面的例子,我们来总结一下关于lambda表达式使用时的一些注意事项:
- (1)lambda表达式要使用引用变量,需要遵守下面的原则:
a. 在调用上下文中的局部变量,只有capture了才可以引用(如上面的例子3所示)
b. 非本地局部变量可以直接引用 - (2)使用者需要注意,closure(lambda表达式生成的可调用实体)引用的变量(主要是指针和引用),在closure调用完成之前,必须保证可用,这一点和上面bind绑定参数之后生成的可调用实体是一致的
- (3)关于lambda的用处,就是用来生成closure,而closure也是一种可调用实体,所以可以通过std::function对象来保存生成的closure,也可以直接用auto
通过上面的介绍,我们基本了解了function, bind和lambda的用法,把三者结合起来,C++将会变得非常强大,有点函数式编程的味道了。最后,这里再补充一点,对于用bind来生成function和用lambda表达式来生成function, 通常情况下两种都是ok的,但是在参数多的时候,bind要传入很多的std::placeholders,而且看着没有lambda表达式直观,所以通常建议优先考虑使用lambda表达式。
熟悉C++98的朋友,应该都知道,在C++98中没有thread, mutex, condition_variable这些与concurrency相关的特性支持,如果需要写多线程相关程序,都要借助于不同平台上各自提供的api,这样带来的问题就是程序的跨平台移植性比较差,经常要用一大堆的#ifdef WIN32类似的宏来区分不同的平台,搞得程序很难看。C++0x最原始的初衷之一就是为了让C++的功能更加强大,更加方便使用。现如今硬件如此发达,concurrency在程序设计中已经是司空见惯的事情了,如果C++再不支持这些concurrency相关的特性,就真的out了。现在,C++程序员的福音到了,C++0x提供了对thread, mutex, condition_variable这些concurrency相关特性的支持,以后多线程这一块的代码可以完全跨平台了,而且由于C++0x封装的都比较好,代码写起来也十分简洁。下面开始介绍今天的内容。
- 1. thread
写过多线程程序的朋友,相信对thread本身都不会陌生,这里不对thread本身做太多的说明,以介绍C++0x中提供的thread的用法为主。请大家先看下面的例子:
#include < iostream> #include < string> #include < thread> class Printer { public: void Print(int id, std::string& name) { std::cout < < "id=" << id << ", name=" << name; } }; void Hello() { std::cout << "hello world" << std::endl; } int main() { Printer p; int id = 1; std::string name("xiao5ge"); std::thread t1(&Printer::Print, p, id, name); std::thread t2(std::bind(&Printer::Print, p, id, name)); std::thread t3([&]{ p.Print(id, name); }); std::thread t4(Hello); t4.join(); t3.join(); t2.join(); t1.join(); }
下面我们来通过分析上面的例子,来说明一下thread的用法。上面的t1-t4的四个例子,分别是thread的四种构造方式,我们一一来介绍一下:- (1)这种方式是通过变参数模板实现的,第一个参数是线程入口函数的地址,后面的参数按函数调用时的参数顺序传入。这里需要说明两点:一是变参模板也是C++0x新增的特性,将会在后面的文章中介绍;二是,类成员函数的第一个参数永远是this,所以这里第一个参数放的是对象本身。
- (2)这种方式是传入一个std::function对象,bind/function在前一篇中有介绍,不熟悉的朋友可以先看一下C++0x系列的第四篇。
- (3)这种方式是传入一个lambda表达式,也即一个closure,是比较常用的方式。关于lambda也在上一篇中有介绍。
- (4)这种方式是最简单,最常用的方式,直接传入一个函数,写过多线程程序的朋友应该对这种方式最为熟悉。
上面介绍了C++0x thread的基本用法,下面需要再补充几点使用过程需要注意的事项:
- (1)如果入口函数的参数是以引用或指针形式传入的,需要使用者保证在线程运行过程中,这些参数一直是有效的。同时,如果有多个线程会访问或者修改这些变量,需要使用者做好同步,保证一致性。
- (2)关于上面提到的几种构造方式,简单的直接用4,复杂的推荐的选择顺序:1->3->2,即变参模板方式->lambda方式->bind方式
- (3)通常需要等子线程运行完,主线程才退出,所以在主线程中通常需要调各子线程的join()。
- 2. mutex
mutex实现的是“互斥锁”的语义,在多线程的程序中,经常需要通过锁的机制来保证数据的一致性,C++0x提供了下面四种语义的mutex:
- (1) std::mutex: 普通的互斥锁,不能递归使用
- (2) std::timed_mutex:带超时的互斥锁,不能递归使用
- (3) std::recursive_mutex:递归互斥锁
- (3) std::recursive_timed_mutex:带超时的递归互斥锁
关于mutex的使用,我们通常建议使用RAII(Resource Acquisition is Initialization)的方式,即在构造的时候lock, 析构的时候unlock, 不建议直接显式的lock/unlock,因为这样比较容易出错。因此,C++0x也提供了两个工具类std::lock_guard和std::unique_lock来辅助我们使用mutex,下面我们通过例子来看一下具体的使用:
#include < mutex> // global vars int data = 0; std::mutex data_mutex; // thread 1 { std::lock_guard< std::mutex> locker(data_mutex); data = 1; } // thread 2 { std::lock_guard< std::mutex> locker(data_mutex); data = 2; }
从上面的例子,相信大家可以对mutex的基本使用方法都应该比较清楚了,由于mutex本身就比较简单,这里不再赘言。说一下std::unique_lock和std::lock_guard的区别,std::lock_guard只允许RAII方式的使用,而std::unique_lock可以在构造之后调用lock/unlock, 更加灵活一些,但使用的时候出错的机率也更大一些,所以如果没有什么特殊的需求,通常推荐尽量使用std::lock_guard. - 3. condition_variable
关于condition_variable,它的语义是今天讲的三个内容里面相对复杂一些的,我在之前也写过一篇关于它的文章,不熟悉的朋友可以先阅读一下《条件变量(Condition Variable)详解》这篇文章,先了解一下条件变量,以方便理解后面的内容。我们知道,条件变量主要是用在多线程之间修改了shared_data之后的相互通信,由于条件变量在多线程编程中非常有用,所以C++0x也添加了对条件变量的支持,下面是C++0x提供的两种不同类型的条件变量:
- (1)condition_variable: 用在std::unique_lock< std::mutex>上wait, 比较高效。
- (2)condition_variable_any: 可以用在任意mutex上wait, 比较灵活,但效率比condition_variable差一些。
下面我们通过例子来看看,条件变量在C++0x中的使用方式:
// global std::atomic< bool> is_finish(false); std::mutex finish_mutex; std::condition_variable finish_cond; // thread 1 { std::unique< std::mutex> locker(finish_mutex); // 1. loop wait while (!is_finish) { finish_cond.wait(locker); } // 2. wait until prediction is true, loop inside finish_cond.wait(locker, []{ return is_finish; }); // 3. wait until eithor prediction is true or timeout finish_cond.wait(locker, std::chrono::seconds(1), []{ return is_finish; }); } // thread 2 { is_finish = true; // 1. notify one of the waiter finish_cond.notify_one(); // 2. notify all the waiter finish_cond.notify_all(); }
上面的例子,基本覆盖了C++0x提供的条件变量的主要用法。下面我们来一一分析一下,帮助大家更好的理解:- (1)关于wait,有三种基本的用法:第1种是在指定的条件上循环等待,直到条件为真notify时才会继续执行后面的逻辑;第2种用法语义上和第1种是一样的,但是不是用户做显式的loop等待,用户传入一个需要满足的条件的closure, wait一直等到这个条件为真被notify时才会返回继续执行下面的逻辑,可以理解为这时候,在wait内部有一个loop;第3 种用法,加了一个超时的语义,wait一直等到条件为真或者超时,被notify时才会返回继续执行下面的逻辑。
- (2)关于notify, 有两种:第1种是notify_one, 只唤醒一个在wait的线程; 第2种是notify_all,唤醒所有在wait的线程,相当于一个broadcast的语义。
熟悉printf/scanf的朋友应该对C/C++中的变参函数不陌生,它可以支持任意个数的参数。模板(template)在C++中的地位相信不用我再多说,但是,一直以来,C++模板不支持变参,这成了模板一个被诟病的地方,在很多使用场景限制了模板的威力。举例来说,同样语义的模板函数,因为参数个数不定,有若干个,这样的场景之下,我们只能通过枚举的方式来实现。这样的方法,一方面使得代码本身不够简洁,另一方面因为枚举只能做到有限个数的参数,还是在一定程度上限制了使用的灵活性。现在,C++0x引入了变参模板,上面讲的问题便迎刃而解了,这对C++程序员朋友们来说是莫大的福音。下面我们先看一个简单的例子,先对变参模板有一个直观的印象:
#include < iostream> #include < string> void Print() { std::cout < < "\n"; } template< typename T, typename ... TRest> void Print(const T& obj, const TRest& ... rest) { std::cout < < obj << " "; Print(rest ...); } int main() { double p = 3.14; std::string s("pi"); Print(p, &p, s, &s); }
上面的例子应该比较容易看懂,实现了一个变参数的Print()函数,不知道大家有没发现,上面的实现有点递归的味道,第一个没有参数的版本,相当于一个初始值,接下来的变参模板版本相关的通用的递归过程。另外,在编译的时候要加上-std=c++0x,要不然编译会有问题,这点需要大家注意一下。
相信通过上面的例子,大家对变参模板应该已经有了一个初步的印象。下面我们来看看变参模板使用过程中的一些需要注意的点。首先需要讲一个基本的概念----Parameter Pack(参数包),它是理解变参模板的基础,通俗地讲,它指的就是变参模板中用来表示可变参数的实体。具体说来,它包括两种基本类型,一种是Template Parameter Pack,另一种是Function Parameter Pack。Template Parameter Pack是可以接受多个模板参数的Parameter Pack, Function Parameter Pack是可以接受多个函数参数的Parameter Pack。如下面的例子所示, 其中Types为Template Parameter Pack, args为Function Parameter Pack:
template< typename ... Types > struct Tuple; Tuple<> t0; // Types contains no arguments Tuple< int> t1; // Types contains 1 argument Tuple< int, float> t2; // Types contains 2 arguments template< typename ... Types > void Func(Types ... args); Func(); // args contains no arguments Func(1); // args contains 1 argument Func(2, 1.0); // args contains 2 argument
理解了变参模板的Parameter Pack,接下来我们来看看,如何把Parameter Pack展开,以为我们所用,这部分内容也是变参模板最为核心的内容。先看下面的例子:template< typename ... Types > f(Types ... args); template< typename ... Types > g(Types ... args) { // "&args ..." is a pack expansion // "&args" is the pattern f(&args ...); } int a = 10; std::string s("hello"); g(a, s) ==> f(&a, &s);
上面的例子中,提到两个概念,一个是“pack expansion",即参数包的展开;一个是"pattern",即展开时的模式。在变参模板中,参数在实例化的时候被展开,展开按指定的pattern方式进行。如上面的例子中所示,g的实现中调用了f, 在这里传给f的参数是传给g的每个参数取地址的结果。相信到这里,大家对Parameter Pack的展开,及其所用的Pattern已经有了一个初步的认识。在各种不同场景下,Parameter Pack展开所采用的Pattern是不一样的,下面是C++0x的文档提供的一些场景所对应的Pattern,在这里列出来,帮助大家更好的认识变参模板Parameter Pack的展开:- — In an initializer-list (8.5); the pattern is an initializer-clause.
- — In a base-specifier-list (Clause 10); the pattern is a base-specifier.
- — In a mem-initializer-list (12.6.2); the pattern is a mem-initializer.
- — In a template-argument-list (14.3); the pattern is a template-argument.
- — In a dynamic-exception-specification (15.4); the pattern is a type-id.
- — In an attribute-list (7.6.1); the pattern is an attribute.
- — In an alignment-specifier (7.6.2); the pattern is the alignment-specifier without the ellipsis.
- — In a capture-list (5.1.2); the pattern is a capture.