C++11的重大改变

自从C++语言第一次迭代已经过去13年。C++标准委员会成员Danny Kalev在本文中解释了这门编程语言有怎样的改进,以及如何帮助你编写更好的代码。
C++的发明者Bjarne Stroustrup最近说,C++11“感觉就像是一个新的语言——各部分之间能够更好的协作”。事实上,核心C++11已经有了很大的改变。现在它支持lambda表达式,自动类型推断,统一的初始化语法,委托构造函数,已删除和默认函数声明,nullptr,以及最重要的右值引用——一种预言将会改变创造和处理对象方式的技术。下面我们将一一说明。
C++11标准库同样增加了新的算法,新的容器类,原子运算,类型特性,正则表达式,新的智能指针,async()功能以及多线程库。
在这篇文章中,我将解释这门语言的重大改变,以及为什么把它们看出是一件大事。就像你即将看到的那样,线程库不是唯一的改变。 新的标准建立在数十年的专家建议基础之上,让C++变得更加有意义。正如Rogers Cadenhead指出的那样,“这就像迪斯科、Pet Rocks(一种古老的美国游戏)和大龄的奥林匹克游泳运动员一样,C++ 表现得相当令人惊喜。”
首先我们来看看C++11突出的核心语言特性。
Lambda 表达式
lambda 表达式允许你定义本地(也就是调用函数的地方)函数,因此可以排除掉许多函数对象导致的冗余以及安全风险。lambda表达式格式为:

[capture](parameters)->return-type {body}

在一个函数调用的参数列表中出现[ ]意味着开始一个lambda表达式。下面看一个lambda例子。

假设你想要计算一个字符串中有多少大写字母。使用for_each()遍历字符数组,下面的 lambda 表达式判断每一个字母是不是大写的。每找到一个大写字母,该lambda表达式将定义在表达式外的Uppercase变量的数值增加:

int main()
{
    char s[] = "Hello World!";
    int Uppercase = 0; // lambda对其进行修改
    for_each(s, s+sizeof(s), [&Uppercase] (char c) {
        if (isupper(c))
        Uppercase++;
    });
    cout << Uppercase << " uppercase letters in: " << s << endl;
}

这就像你定义了一个函数,而函数体位于另外一个函数调用中。[&Uppercase]中的&意味着lambda体以引用方式获得Uppercase所以能够修改它。没有&符号,Uppercase将以传值的方式传入。C++11 lambda也包含了对成员函数的构造。


自动类型推断auto和decltype

在C++03中,你必须在声明时指定对象类型。但是在许多情况下,对象的声明包含了一个初始化器。C++11利用这一地点,允许你以不指定类型的方式声明对象:

auto x = 0; // 因为 0 是 int,所以 x 类型是 int
auto c = 'a'; // char
auto d = 0.5; // double
auto national_debt = 14400000000000LL; //long long

当对象类型太冗长或者是在模板中自动生成时,自动类型推断尤其有用。考虑:

void func(const vector<int> &vi)
{
    vector<int>::const_iterator ci = vi.begin();
}

你可以像下面一样声明这个遍历器:

auto ci = vi.begin();

auto关键字不是新增的,早在ANSI C之前就已经存在。但是 C++11改变了其含义:auto不在意味着一个具有自动存储类型的对象。它意味着一个从其初始化器推断类型的对象。auto旧的语义已经从C++11移除,以避免产生歧义。

C++11 为捕获对象或表达式的类型提供了类似的机制。新的运算符decltype接收一个表达式并“返回”其类型:

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

统一初始化语法

C++至少有四种不同的初始化方法,有些是重叠的。

带有括号的初始化类似:

std::string s("hello");
int m = int(); // 默认初始化

在某些特殊情况,你也可以使用“=”达到相同目的:

std::string s = "hello";
int x = 5;

对于 POD(Plain Old Data,具有 C 兼容特点)聚合,可以使用大括号:

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

最后,在构造函数中使用成员初始化器:

struct S
{
    int x;
    S(): x(0) {}
};

初始化操作的多种变体是令人感觉困扰的重要原因之一,不仅仅对于新手而言。更糟糕的是,在C++03你不能对使用new[]分配空间的POD数组进行数组成员和POD本身的初始化。C++11使用统一的大括号标记清楚了这种混乱:

class C
{
    int a;
    int b;
public:
    C(int i, int j);
};

C c {0,0}; // C++11可用,等价于C c(0,0);

int* a = new int[3] { 1, 2, 0 }; // C++11可用

class X
{
    int a[4];
public:
    X() : a{1,2,3,4} {} // C++11 可用,成员数组初始化器
};

对于容器,现在可以跟一长串push_back()调用说再见了。在C++11你可以直观地初始化容器:
// C++11容器初始化器

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

类似的,C++11支持数据成员的类内初始化:

class C
{
    int a = 7; // C++11可用
public:
    C();
};

删除和默认函数

具备如下形式的函数成为默认函数(defaulted function):

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

=default;部分说明,编译器生成该函数的一个默认实现。默认函数有两个优势:比手工实现更有效;把程序员从手动定义这些函数的繁琐中解放出来。

与默认函数相反的是删除函数:

int func() = delete;

删除函数对防止对象复制很有用。回忆一下,C++为每个类自动声明一个拷贝构造函数和赋值运算符。为禁止复制,需要将这两个特殊的成员函数声明为=delete:

struct NoCopy
{
    NoCopy & operator =( const NoCopy & ) = delete;
    NoCopy ( const NoCopy & ) = delete;
};
NoCopy a;
NoCopy b(a); // 编译错误,拷贝构造函数已经被删除

nullptr

终于,C++有了一个代表空指针常量的关键字。nullptr替换了充满bug的NULL宏,以及被用于空指针好多年的字面常量0。nullptr是强类型的:

void f(int); // #1
void f(char *);// #2
// C++03
f(0); // 调用哪一个 f?
// C++11
f(nullptr) // 无歧义,调用 #2

nullptr适用于所有指针类型,包括函数指针和成员指针:

const char *pc = str.c_str(); // 数据指针
if (pc != nullptr)
    cout << pc << endl;
int (A::*pmf)() = nullptr; // 成员函数指针
void (*pmf)() = nullptr; // 函数指针

委托构造函数

在 C++11中,构造函数可以调用同一个类中另外的构造函数:

class M // C++11委托构造函数
{
    int x, y;
    char *p;
public:
    M(int v) : x(v), y(0),  p(new char [MAX])  {} // #1 目标
    M(): M(0) {cout << "delegating ctor" << endl;} // #2 委托
};

构造函数#2是委托构造函数,调用了目标构造函数#1。


右值引用

C++03的引用类型只能绑定左值。C++11引入了新的引用类型——右值引用。右值引用可以绑定右值,例如临时变量和字面常量。

增加右值引用的主要原因是移动语义。不同于传统的拷贝,移动的含义是目标对象占有源对象的资源,将源对象设置为“空”状态。在这种情景下,拷贝一个对象既昂贵又不必要,应该使用移动操作符。为感受移动语义在性能上的优势,考虑交换字符串。一个原始的实现类似于:

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

这种实现很昂贵。拷贝字符串需要分配内存,将字符从源对象复制到目标对象。相比而言,移动字符串仅仅意味着交换两个数据成员,不需要分配内存、复制字符数组和释放内存:

void moveswapstr(string& empty, string & filled)
{
    // 伪代码,体会思想
    size_t sz = empty.size();
    const char *p = empty.data();
    // 移动filled的资源到empty
    empty.setsize(filled.size());
    empty.setdata(filled.data());
    // 移动empty的资源到filled
    filled.setsize(sz);
    filled.setdata(p);
}

如果你正在实现一个支持移动的类,需要声明一个移动构造函数和移动复制运算符:

class Movable
{
    Movable (Movable&&); // 移动构造函数
    Movable&& operator=(Movable&&); // 移动复制运算符
};

C++11 标准库大量使用了移动语义。许多算法和容器也为移动语义做了优化。


C++11 标准库

2003年,C++以库技术报告 1(TR1)的形式经历了一次大型重构。TR1包含了新的容器类(unordered_set,unordered_map,unordered_multiset和unordered_multimap)和许多新的库,例如正则表达式,元祖,函数对象包装器。随着C++11的颁布,TR1连同新的库一起正式集成到C++标准中。下面是C++11标准库的特性:


线程库

毫无疑问,从程序员角度看,C++11最重要的改进就是并发。C++11有一个thread类,描述一个执行线程、promise和future(用于并发环境下同步的对象),用于发起并发任务的模板函数async()和用于声明线程独立的数据的存储类型thread_local。快速了解C++11线程库,请阅读Anthony Williams的文章Simpler Multithreading in C++0x。


新的智能指针类

C++98只定义了一个智能指针类,auto_ptr,而这个类现在已经被废弃了。C++11包含了新的智能指针类:shared_ptr和最近新加的unique_ptr。这两个类都与其他标准库组件兼容,所以你可以安全地将这些智能指针添加到标准容易以及使用标准算法操作。


新的算法

C++11标准库定义了模拟集合论操作的新的算法all_of()、any_of()和none_of()。下面几行将谓词ispositive()应用于范围[first, first+n),然后使用all_of()、any_of()和none_of()检测范围的属性:

#include <algorithm>
// C++11 代码
// 所有元素都是正数吗?
all_of(first, first+n, ispositive()); // false
// 至少有一个元素是正数吗?
any_of(first, first+n, ispositive()); // true
// 没有元素是正数?
none_of(first, first+n, ispositive()); // false

还有一个新的copy_n算法。使用copy_n()将一个有 5 个元素的数组复制到另外一个可说是小菜一碟:

#include <algorithm>
int source[5] = { 0, 12, 34, 50, 80 };
int target[5];
// 从源数组到目的数组拷贝 5 个元素
copy_n(source, 5, target);

iota()算法创建一个递增的数字范围,就像首先给*first赋初始值,然后使用++递增。在下面的代码中,iota()将连续数值{10,11,12,13,14}赋值给数组a,将{'a', 'b', 'c'}赋值给字符数组c。

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

C++11依然缺少一些有用的库,例如 XML API,socket,GUI,反射——当然,还有合理的自动垃圾回收器。但是现在C++的确已经提供了很多新的特性,使得代码更加安全、高效、以及学习和使用起来更加简单。

如果C++11的改变过于宏大,不要抱怨。花些时间循序渐进地理解这些变化。在这一过程的最后,你可能就同意Stroustrup的意见:C++11的确像是一种新的语言——一种更好的语言!

原文链接:https://www.devbean.net/2012/11/biggest-changes-in-c11/

© 版权声明
THE END
喜欢就支持一下吧
点赞948 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容