C++ 0X

本文详细介绍了C++17中的五大新特性,包括统一初始化语法、auto关键字的使用、基于范围的for循环、nullptr类型和对unicode的支持。通过对比C++98和C++17的差异,深入理解C++17的改进之处。
摘要由CSDN通过智能技术生成

摘自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最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等
  • 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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值