C++11 中值得关注的几大变化

源文章来自前C++标准委员会的 Danny KalevThe Biggest Changes in C++11 (and Why You Should Care),赖勇浩作了一个中文翻译在这里。因此,我就不翻译了,我在这里仅对文中提到的这些变化“追问为何要引入这些变化”的一个探讨,只有知道为了什么,用在什么地方,咱们才能真正学到这个知识。而以此你能够更深刻地了解这些变化。因此,本文不是翻译。由于写得有些仓促,因此不免有问题,还请你们指正。

Lambda 表达式

Lambda表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫“闭包”,若是 lambda 函数没有传回值(例如 void ),其回返类型可被彻底忽略。 定义在与 lambda 函数相同做用域的变量参考也能够被使用。这种的变量集合通常被称做 closure(闭包)。我在这里就再也不讲这个事了。表达式的简单语法以下,css

[capture](parameters)->return_type {body}

原文的做者给出了下面的例子:html

int main() { char s[] = "Hello World!"; int Uppercase = 0; //modified by the lambda for_each(s, s + sizeof(s), [&Uppercase](char c) { if (isupper(c)) Uppercase++; }); cout << Uppercase << " uppercase letters in: " << s << endl; }

在传统的STL中for_each() 这个玩意最后那个参数须要一个“函数对象”,所谓函数对象,实际上是一个class,这个class重载了operator(),因而这个对象能够像函数的式样的使用。实现一个函数对象并不容易,须要使用template,好比下面这个例子就是函数对象的简单例子(实际的实现远比这个复杂):正则表达式

template <class T> class less { public: bool operator()(const T&l, const T&r)const { return l < r; } };

因此,C++引入Lambda的最主要缘由就是1)能够定义匿名函数,2)编译器会把其转成函数对象。相信你会和我同样,会疑问为何之前STL中的ptr_fun()这个函数对象不能用?(ptr_fun()就是把一个天然函数转成函数对象的)。缘由是,ptr_fun() 的局限是其接收的天然函数只能有1或2个参数。算法

那么,除了方便外,为何必定要使用Lambda呢?它比传统的函数或是函数对象有什么好处呢?我我的所理解的是,这种函数之年以叫“闭包”,就是由于其限制了别人的访问,更私有。也能够认为他是一次性的方法。Lambda表达式应该是简洁的,极私有的,为了更易的代码和更方便的编程。编程

自动类型推导 auto

在这一节中,原文主要介绍了两个关键字 auto 和 deltype,示例以下:数组

auto x=0; //x has type int because 0 is int auto c='a'; //char auto d=0.5; //double auto national_debt=14400000000000LL;//long long 

auto 最大的好处就是让代码简洁,尤为是那些模板类的声明,好比:STL中的容器的迭代子类型。promise

vector<int>::const_iterator ci = vi.begin();

能够变成:安全

auto ci = vi.begin();

模板这个特性让C++的代码变得很难读,不信你能够看看STL的源码,那是一个乱啊。使用auto必需一个初始化值,编译器能够经过这个初始化值推导出类型。由于auto是来简化模板类引入的代码难读的问题,如上面的示例,iteration这种类型就最适合用auto的,可是,咱们不该该把其滥用。闭包

好比下面的代码的可读性就下降了。由于,我不知道ProcessData返回什么?int? bool? 仍是对象?或是别的什么?这让你后面的程序不知道怎么作。并发

auto obj = ProcessData(someVariables);

可是下面的程序就没有问题,由于pObject的型别在后面的new中有了。

auto pObject = new SomeType<OtherType>::SomeOtherType();
自动化推导 decltype

关于 decltype 是一个操做符,其能够评估括号内表达式的类型,其规则以下:

  • 若是表达式e是一个变量,那么就是这个变量的类型。
  • 若是表达式e是一个函数,那么就是这个函数返回值的类型。
  • 若是不符合1和2,若是e是左值,类型为T,那么decltype(e)是T&;若是是右值,则是T。

    原文给出的示例以下,咱们能够看到,这个让的确咱们的定义变量省了不少事。

    const vector<int> vi; typedef decltype (vi.begin()) CIT; CIT another_const_iterator; 

    还有一个适合的用法是用来typedef函数指针,也会省不少事。好比:

    decltype(&myfunc) pfunc = 0; typedef decltype(&A::func1) type; 
    auto 和 decltype 的差异和关系

    Wikipedia 上是这么说的(关于decltype的规则见上)

    #include <vector> int main() { const std::vector<int> v(1); auto a = v[0]; // a 的类型是 int decltype(v[0]) b = 1; // b 的类型是 const int&, 由于函数的返回类型是 // std::vector<int>::operator[](size_type) const auto c = 0; // c 的类型是 int auto d = c; // d 的类型是 int decltype(c) e; // e 的类型是 int, 由于 c 的类型是int decltype((c)) f = c; // f 的类型是 int&, 由于 (c) 是左值 decltype(0) g; // g 的类型是 int, 由于 0 是右值 } 

    若是auto 和 decltype 在一块儿使用会是什么样子?能看下面的示例,下面这个示例也是引入decltype的一个缘由——让C++有能力写一个 “ forwarding function 模板”,

    template< typename LHS, typename RHS> auto AddingFunc(const LHS &lhs, const RHS &rhs) -> decltype(lhs+rhs) {return lhs + rhs;} 

    这个函数模板看起来至关费解,其用到了auto 和 decltype 来扩展了已有的模板技术的不足。怎么个不足呢?在上例中,我不知道AddingFunc会接收什么样类型的对象,这两个对象的 + 操做符返回的类型也不知道,老的模板函数没法定义AddingFunc返回值和这两个对象相加后的返回值匹配,因此,你可使用上述的这种定义。

    统一的初始化语法

    C/C++的初始化的方法比较,C++ 11 用大括号统一了这些初始化的方法。

    好比:POD的类型。

    int arr[4]={0,1,2,3}; struct tm today={0}; 

    关于POD相说两句,所谓POD就是Plain Old Data,当class/struct是极简的(trivial)、属于标准布局(standard-layout),以及他的全部非静态(non-static)成员都是POD时,会被视为POD。如:

    struct A { int m; }; // POD struct B { ~B(); int m; }; // non-POD, compiler generated default ctor struct C { C() : m() {}; ~C(); int m; }; // non-POD, default-initialising m 

    POD的初始化有点怪,好比上例,new A; 和new A(); 是不同的,对于其内部的m,前者没有被初始化,后者被初始化了(不一样 的编译器行为不同,VC++和GCC不同)。而非POD的初始化,则都会被初始化。

    从这点能够看出,C/C++的初始化问题很奇怪,因此,在C++ 2011版中就作了统一。原文做者给出了以下的示例:

    C c {0,0}; //C++11 only. 至关于: C c(0,0); int* a = new int[3] { 1, 2, 0 }; /C++11 only class X { int a[4]; public: X() : a{1,2,3,4} {} //C++11, member array initializer }; 

    容器的初始化:

    // C++11 container initializer vector<string> vs={ "first", "second", "third"}; map singers = { {"Lady Gaga", "+1 (212) 555-7890"} ,{"Beyonce Knowles", "+1 (212) 55-0987"} }; 

    还支持像Java同样的成员初始化:

    class C { int a=7; //C++11 only public: C(); }; 
    Delete 和 Default 函数

    咱们知道C++的编译器在你没有定义某些成员函数的时候会给你的类自动生成这些函数,好比,构造函数,拷贝构造,析构函数,赋值函数。有些时候,咱们不想要这些函数,好比,构造函数,由于咱们想作实现单例模式。传统的作法是将其声明成private类型。

    在新的C++中引入了两个指示符,delete意为告诉编译器不自动产生这个函数,default告诉编译器产生一个默认的。原文给出了下面两个例子:

    struct A { A()=default; //C++11 virtual ~A()=default; //C++11 }; 

    再如delete

    struct NoCopy { NoCopy & operator =( const NoCopy & ) = delete; NoCopy ( const NoCopy & ) = delete; }; NoCopy a; NoCopy b(a); //compilation error, copy ctor is deleted 

    这里,我想说一下,为何咱们须要default?我什么都不写不就是default吗?不全然是,好比构造函数,由于只要你定义了一个构造函数,编译器就不会给你生成一个默认的了。因此,为了要让默认的和自定义的共存,才引入这个参数,以下例所示:

    struct SomeType { SomeType() = default; // 使用编译器生成的默认构造函数 SomeType(OtherType value); }; 

    关于delete还有两个有用的地方是

    1)让你的对象只能生成在栈内存上:

    struct NonNewable { void *operator new(std::size_t) = delete; }; 

    2)阻止函数的其形参的类型调用:(若尝试以 double 的形参调用 f(),将会引起编译期错误, 编译器不会自动将 double 形参转型为 int 再调用f(),若是传入的参数是double,则会出现编译错误)

    void f(int i); void f(double) = delete; 
    nullptr

    C/C++的NULL宏是个被有不少潜在BUG的宏。由于有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。可是在C++的时代,这就会引起不少问题。你能够上网看看。这是为何须要 nullptr 的缘由。 nullptr 是强类型的。

    void f(int); //#1 void f(char *);//#2 //C++03 f(0); //二义性 //C++11 f(nullptr); //无二义性,调用f(char*) 

    因此在新版中请以 nullptr 初始化指针。

    委托构造

    在之前的C++中,构造函数之间不能互相调用,因此,咱们在写这些类似的构造函数里,咱们会把相同的代码放到一个私有的成员函数中。

    class SomeType {
    private:
        int number;
        string name;
        SomeType( int i, string&amp; s ) : number(i), name(s){}
    public:
        SomeType( )               : SomeType( 0, "invalid" ){}
        SomeType( int i )         : SomeType( i, "guest" ){}
        SomeType( string&amp; s ) : SomeType( 1, s ){ PostInit(); }
    };

    可是,为了方便并不足让“委托构造”这个事出现,最主要的问题是,基类的构造不能直接成为派生类的构造,就算是基类的构造函数够了,派生类还要本身写本身的构造函数:

    class BaseClass
    {
    public:
        BaseClass(int iValue);
    };
    
    class DerivedClass : public BaseClass
    {
    public:
        using BaseClass::BaseClass;
    };

    上例中,派生类手动继承基类的构造函数, 编译器可使用基类的构造函数完成派生类的构造。 而将基类的构造函数带入派生类的动做 没法选择性地部分带入, 因此,要不就是继承基类所有的构造函数,要不就是一个都不继承(不手动带入)。 此外,若牵涉到多重继承,从多个基类继承而来的构造函数不能够有相同的函数签名(signature)。 而派生类的新加入的构造函数也不能够和继承而来的基类构造函数有相同的函数签名,由于这至关于重复声明。(所谓函数签名就是函数的参数类型和顺序不)

    右值引用和move语义

    在老版的C++中,临时性变量(称为右值”R-values”,位于赋值操做符之右)常常用做交换两个变量。好比下面的示例中的tmp变量。示例中的那个函数须要传递两个string的引用,可是在交换的过程当中产生了对象的构造,内存的分配还有对象的拷贝构造等等动做,成本比较高。

    void naiveswap(string a, string b)
    {
        string temp = a;
        a=b;
        b=temp;
    }

    C++ 11增长一个新的引用(reference)类型称做右值引用(R-value reference),标记为typename &&。他们可以以non-const值的方式传入,容许对象去改动他们。这项修正容许特定对象创造出move语义。

    举例而言,上面那个例子中,string类中保存了一个动态内存分存的char*指针,若是一个string对象发生拷贝构造(如:函数返回),string类里的char*内存只能经过建立一个新的临时对象,并把函数内的对象的内存copy到这个新的对象中,而后销毁临时对象及其内存。这是原来C++性能上重点被批评的事

    能过右值引用,string的构造函数须要改为“move构造函数”,以下所示。这样一来,使得对某个stirng的右值引用能够单纯地从右值复制其内部C-style的指针到新的string,而后留下空的右值。这个操做不须要内存数组的复制,并且空的暂时对象的析构也不会释放内存。其更有效率。

    class string
    {
        string (string&&); //move constructor
        string&& operator=(string&&); //move assignment operator
    };

    The C++11 STL中普遍地使用了右值引用和move语议。所以,不少算法和容器的性能都被优化了。

    C++ 11 STL 标准库

    C++ STL库在2003年经历了很大的整容手术 Library Technical Report 1 (TR1)。 TR1 中出现了不少新的容器类 (unordered_set, unordered_map, unordered_multiset, 和 unordered_multimap) 以及一些新的库支持诸如:正则表达式, tuples,函数对象包装,等等。 C++11 批准了 TR1 成为正式的C++标准,还有一些TR1 后新加的一些库,从而成为了新的C++ 11 STL标准库。这个库主要包含下面的功能:

    线程库

    这们就很少说了,之前的STL饱受线程安全的批评。如今好 了。C++ 11 支持线程类了。这将涉及两个部分:第1、设计一个可使多个线程在一个进程中共存的内存模型;第2、为线程之间的交互提供支持。第二部分将由程序库提供支持。你们能够看看promises and futures,其用于对象的同步。 async() 函数模板用于发起并发任务,而 thread_local 为线程内的数据指定存储类型。更多的东西,能够查看 Anthony Williams的 Simpler Multithreading in C++0x.

    新型智能指针

    C++98 的知能指针是 auto_ptr, 在C++ 11中被废弃了。C++11  引入了两个指针类: shared_ptrunique_ptr。 shared_ptr只是单纯的引用计数指针,unique_ptr 是用来取代auto_ptrunique_ptr 提供 auto_ptr 大部份特性,惟一的例外是 auto_ptr 的不安全、隐性的左值搬移。不像 auto_ptrunique_ptr 能够存放在 C++0x 提出的那些能察觉搬移动做的容器之中。

    为何要这么干?你们能够看看《More Effective C++》中对 auto_ptr的讨论。

    新的算法

    定义了一些新的算法: all_of(), any_of()none_of()。

    #include <algorithm>
    
    //C++11 code
    //are all of the elements positive?
    all_of(first, first+n, ispositive()); //false
    //is there at least one positive element?
    any_of(first, first+n, ispositive());//true
    // are none of the elements positive?
    none_of(first, first+n, ispositive()); //false

    使用新的copy_n()算法,你能够很方便地拷贝数组。

    #include <algorithm>
    int source[5]={0,12,34,50,80};
    int target[5];
    //copy 5 elements from source to target
    copy_n(source,5,target);

    使用 iota() 能够用来建立递增的数列。以下例所示:

    include <numeric>
    
    int a[5]={0};
    char c[3]={0};
    iota(a, a+5, 10); //changes a to {10,11,12,13,14}
    iota(c, c+3, 'a'); //{'a','b','c'}

    总之,看下来,C++11 仍是很学院派,不少实用的东西仍是没有,好比: XML,sockets,reflection,固然还有垃圾回收。看来要等到C++ 20了。呵呵。不过C++ 11在性能上仍是很快。参看 Google’s benchmark tests。原文还引用Stroustrup 的观点:C++11 是一门新的语言——一个更好的 C++。

    若是把全部的改变都列出来,你会发现真多啊。我估计C++ Primer那本书的厚度要增长至少30%以上。C++的门槛会不会愈来愈高了呢?我不知道,但我我的以为这门语言的确是变得愈来愈使人望而却步了。(想起了某人和我说的一句话——学技术真的是太累了,仍是搞方法论好混些?)

    (全文完)

  • 相关文章
    相关标签/搜索