摘自http://www.wuzesheng.com/?p=1972
参考http://www.ibm.com/developerworks/cn/aix/library/au-gcc/
大家先看看下面的代码,一个我们比较熟悉的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 }
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*
用过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
在具体介绍这个特性之前,我们首先来看一下,在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()及迭代器遍历也可以用
我们知道,在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
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>
在C++98中,有控制字符和转义字符的概念,像’\n’用来回车等,但有的时候,我们就想把像控制字符这样的特殊字符原样的传输出,不做特殊处理,这时候就需要转义,显得比较麻烦,尤其在正则表达式中,格外突出。C++0x增加了raw string这个概念,也叫原字符串,这样的字符串没有特殊字符,所有的字符都是普通字符,请看下面的例子:
std::string noNewlines(R"(\n\n)"); std::string cmd(R"(ls /home/docs | grep ".pdf")");
std::regex re1(R"!("operator\(\)"|"operator->")!"); /// "operator()"| "operator->" std::regex re2(R"xyzzy("\([A-Za-z_]\w*\)")xyzzy"); /// "(identifier)"
我们知道变量的初始化在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); ///< 从别的容器初始化
/// 不能初始化的情况 class Foo { public: Foo() : data(???) {} ///< 不能在这里初始化数组 private: int data[3]; }; int * data = new int[3]; ///< 不能在这里初始化堆上内存
/// 与上面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中的初始化:
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};
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放在一起讲的主要原因,它们三者在使用回调函数的过程中各显神通。下面就为大家一步步接开这三者的神秘面纱。
我们知道,在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");
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)
讲完了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表达式使用时的一些注意事项:
通过上面的介绍,我们基本了解了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封装的都比较好,代码写起来也十分简洁。下面开始介绍今天的内容。
写过多线程程序的朋友,相信对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(); }
上面介绍了C++0x thread的基本用法,下面需要再补充几点使用过程需要注意的事项:
mutex实现的是“互斥锁”的语义,在多线程的程序中,经常需要通过锁的机制来保证数据的一致性,C++0x提供了下面四种语义的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; }
关于condition_variable,它的语义是今天讲的三个内容里面相对复杂一些的,我在之前也写过一篇关于它的文章,不熟悉的朋友可以先阅读一下《条件变量(Condition Variable)详解》这篇文章,先了解一下条件变量,以方便理解后面的内容。我们知道,条件变量主要是用在多线程之间修改了shared_data之后的相互通信,由于条件变量在多线程编程中非常有用,所以C++0x也添加了对条件变量的支持,下面是C++0x提供的两种不同类型的条件变量:
下面我们通过例子来看看,条件变量在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(); }
熟悉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
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);