现在的位置: 首页 > 综合 > 正文

C++11

2013年10月12日 ⁄ 综合 ⁄ 共 11979字 ⁄ 字号 评论关闭

C++11,先前被称作C++0x,即ISO/IEC 14882:2011,是目前的C++编程语言的正式标准。它取代第二版标准ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998公开于1998年,第二版于2003年更新,分别通称C++98以及C++03,两者差异很小)。新的标准包含核心语言的新机能,而且扩展C++标准程序库,并入了大部分的C++
Technical Report 1
程序库(数学的特殊函数除外)。最新的消息被公开在
ISO C++ 委员会网站(英文)

ISOIEC JTC1/SC22/WG21 C++ 标准委员会计划在2010年8月之前完成对最终委员会草案的投票,以及于2011年3月召开的标准会议完成国际标准的最终草案。然而,WG21预期ISO将要花费六个月到一年的时间才能正式发布新的
C++ 标准。为了能够如期完成,委员会决定致力于直至2006年为止的提案,忽略新的提案[1]。最终于2011年8月12日公布,并于2011年9月出版。

2012年2月28日的国际标准草案(N3376)是最接近于现行标准的草案,差异仅有编辑上的修正。

像C++这样的编程语言,通过一种演化的的过程来发展其定义。这个过程不可避免地将引发与现有代码的兼容问题,在C++的发展过程中偶尔会发生。不过根据Bjarne
Stroustrup
(C++的创始人并且是委员会的一员)表示,新的标准将几乎100%兼容于现有标准。

目录

候选变更

C++的修订包含核心语言以及标准程序库。

在发展新标准的每个机能上,委员会采取了几个方向:

  • 维持与C++98,可能的话还有C之间的稳定性与兼容性;
  • 尽可能不通过核心语言的扩展,而是通过标准程序库来引进新的特色;
  • 能够演进编程技术的变更优先;
  • 改进 C++ 以帮助系统以及库设计,而不是引进只针对特别应用的新特色;
  • 增进类型安全,提供对现行不安全的技术更安全的替代方案;
  • 增进直接对硬件工作的能力与表现;
  • 提供现实世界中问题的适当解决方案;
  • 实行“zero-overhead”原则(某些功能要求的额外支持只有在该功能被使用时才能使用);
  • 使C++易于教授与学习

对初学者的注重被认为是重要的,因为他们构成了计算机程序员的主体。也因为许多初学者不愿扩展他们对 C++ 的知识,只限于使用他们对 C++ 专精的部分。此外,考虑到 C++ 被广泛的使用(包含应用领域和编程风格),即便是最有经验的程序员在面对新的编程范式时也会成为初学者。

C++核心语言的扩充

C++委员会的主要焦点是在语言核心的发展上。核心语言将被大幅改善的领域包括多线程(或称为“多线程”)支持、泛型编程、统一的初始化,以及性能表现的加强。

在此分成4个区块来讨论核心语言的特色以及变更: 运行期表现强化、建构期表现强化、可用性强化,还有新的功能。某些特色可能会同时属于多个区块,但在此仅于其最具代表性的区块描述该特色。

核心语言的运行期表现强化

以下的语言机能主要用来提升某些性能表现,像是存储器或是速度上的表现。

右值引用和 move 语义

在 C++03及之前的标准,临时对象(称为右值"R-values",位于赋值运算符之右)无法被改变,在 C 中亦同(且被视为无法和 const T& 做出区分)。尽管在某些情况下临时对象的确会被改变,甚至也被视为是一个有用的漏洞。

C++11 增加一个新的非常数引用(reference)类型,称作右值引用(R-value reference),标记为T &&。右值引用所引用的临时对象可以在该临时对象被初始化之后做修改,这是为了允许 move 语义。

C++03 性能上被长期被诟病的其中之一,就是其耗时且不必要的深度拷贝。深度拷贝会发生在当对象是以传值的方式传递。举例而言,std::vector<T> 是内部保存了 C-style 数组的一个包装,如果一个std::vector<T>的临时对象被建构或是从函数返回,要将其存储只能通过生成新的std::vector<T>并且把该临时对象所有的数据复制进去。该临时对象和其拥有的内存会被摧毁。(为了讨论上的方便,这里忽略返回值优化)

在 C++11,一个std::vector的 "move 构造函数" 对某个vector的右值引用可以单纯地从右值复制其内部 C-style 数组的指针到新的 vector,然后留下空的右值。这个操作不需要数组的复制,而且空的暂时对象的解构也不会摧毁存储器。传回vector暂时对象的函数只需要传回std::vector<T>&&。如果vector没有 move 构造函数,那么复制构造函数将被调用,以const
std::vector<T> &
的正常形式。 如果它确实有 move 构造函数,那么就会调用 move 构造函数,这能够免除大幅的存储器配置。

基于安全的理由,具名的变量将永远不被认定为右值,即使它是被如此声明的;为了获得右值必须使用 std::move<T>()

bool is_r_value(int &&) { return true; }
bool is_r_value(const int &) { return false; }
 
void test(int && i)
{
    is_r_value(i); // i 為具名變數,即使被宣告成右值也不會被認定是右值。
    is_r_value(std::move<int>(i)); // 使用 std::move<T>() 取得右值。
}

由于右值引用的用语特性以及对于左值引用(L-value references;regular references)的某些用语修正,右值引用允许开发者提供完美转发 (perfect function forwarding)。当与变长参数模板结合,这项能力允许函数模板能够完美地转送引数给其他接受这些特定引数的函数。最大的用处在于转送构造函数参数,创造出能够自动为这些特定引数调用正确建构式的工厂函数(factory
function)。

泛化的常数表示式

C++ 本来就已具备常数表示式(constant expression)的概念。像是 3+4 总是会产生相同的结果并且没有任何的副作用。常数表示式对编译器来说是优化的机会,编译器时常在编译期运行它们并且将值存入程序中。同样地,在许多场合下,C++ 规格要求使用常数表示式。例如在数组大小的定义上,以及枚举值(enumerator values)都要求必须是常数表示式。

然而,常数表示式总是在遇上了函数调用或是对象建构式时就终结。所以像是以下的例子是不合法的:

int GetFive() {return 5;}
 
int some_value[GetFive() + 5]// 欲產生 10 個整數的陣列。 不合法的 C++ 寫法

这不是合法的 C++,因为 GetFive() + 5 并不是常数表示式。编译器无从得知 GetFive 实际上在运行期是常数。理论上而言,这个函数可能会影响全局变量,或者调用其他的非运行期(non-runtime)常数函数等。

C++11引进关键字 constexpr 允许用户保证函数或是对象建构式是编译期常数。以上的例子可以被写成像是下面这样:

constexpr int GetFive() {return 5;}
 
int some_value[GetFive() + 5]// 欲產生 10 個整數的陣列。合法的C++11寫法

这使得编译器能够了解并去验证 GetFive 是个编译期常数。

对函数使用 constexpr 在函数可以做的事上面加上了非常严格的条件。首先,该函数的回返值类型不能为 void。第二点,函数的内容必须依照 "returnexpr" 的形式。第三点,在引数取代后,expr 必须是个常数表示式。这些常数表示式只能够调用其他被定义为
constexpr 的函数,或是其他常数表示式的数据变量。 最后一点,有着这样标签的函数直到在该编译单元内被定义之前是不能够被调用的。

变量也可以被定义为常数表示式值:

constexpr double forceOfGravity = 9.8;
constexpr double moonGravity = forceOfGravity / 6.0;

常数表示式的数据变量是隐式的常数。他们可以只存储常数表示式或常数表示式建构式的结果。

为了从用户自定类型(user-defined type)建构常数表示式的数据变量,建构式也可以被声明成 constexpr。与常数表示式函数一样,常数表示式的建构式必须在该编译单元内使用之前被定义。他必须有着空的函数本体。它必须用常数表示式初始化他的成员(member)。而这种类型的解构式应当是无意义的(trivial),什么事都不做。

复制 constexpr 建构起来的类型也应该被定义为 constexpr,这样可以让他们从常数表示式的函数以值传回。类型的任何成员函数,像是复制建构式、重载的运算符等等,只要他们符合常数表示式函数的定义,都可以被声明成constexpr。这使得编译器能够在编译期进行类型的复制、对他们施行运算等等。

常数表示式函数或建构式,可以以非常数表示式(non-constexpr)参数唤起。就如同 constexpr 整数字面值能够指派给 non-constexpr 变量,constexpr 函数也可以接受 non-constexpr 参数,其结果存储于 non-constexpr 变量。constexpr 关键字只有当表示式的成员都是 constexpr,才允许编译期常数性的可能。

对POD定义的修正

在标准C++,一个结构(struct)为了能够被当成plain old data (POD),必须遵守几条规则。有很好的理由使我们想让大量的类型符合这种定义,符合这种定义的类型能够允许产生与C兼容的对象布局(object layout)。然而,C++03的规则太严苛了。

C++11将会放宽关于POD的定义。

当class/struct是极简的(trivial)、属于标准布局(standard-layout),以及他的所有非静态(non-static)成员都是POD时,会被视为POD。

一个极简的类型或结构符合以下定义:

  1. 极简的默认建构式。这可以使用默认建构式语法,例如SomeConstructor() = default;
  2. 极简的复制建构式,可使用默认语法(default syntax)
  3. 极简的赋值运算符,可使用默认语法(default syntax)
  4. 极简的解构式,不可以是虚拟的(virtual)

一个标准布局(standard-layout)的类型或结构符合以下定义:

  1. 只有非静态的(non-static)数据成员,且这些成员也是符合标准布局的类型
  2. 对所有non-static成员有相同的访问控制(public, private, protected)
  3. 没有虚函数
  4. 没有虚拟基类
  5. 只有符合标准布局的基类
  6. 没有和第一个定义的non-static成员相同类型的基类
  7. 若非没有带有non-static成员的基类,就是最底层(继承最末位)的类型没有non-static数据成员而且至多一个带有non-static成员的基类。基本上,在该类型的继承体系中只会有一个类型带有non-static成员。

核心语言建构期表现的加强

外部模板

在标准C++中,只要在编译单元内遇到被完整定义的模板,编译器都必须将其实例化(instantiate)。这会大大增加编译时间,特别是模板在许多编译单元内使用相同的参数实例化。看起来没有办法告诉C++不要引发模板的实例化。

C++11将会引入外部模板这一概念。C++已经有了强制编译器在特定位置开始实例化的语法:

template class std::vector<MyClass>;

而C++所缺乏的是阻止编译器在某个编译单元内实例化模板的能力。C++11将简单地扩充前文语法如下:

extern template class std::vector<MyClass>;

这样就告诉编译器不要在该编译单元内将该模板实例化。

核心语言使用性的加强

这些特色存在的主要目的是为了使C++能够更容易使用。 举凡可以增进类型安全,减少代码重复,不易误用代码之类的。

初始化列表

标准C++从C带来了初始化列表(initializer list)的概念。这个构想是结构或是数组能够依据成员在该结构内定义的顺序通过给予的一串引数来产生。这些初始化列表是递归的,所以结构的数组或是包含其他结构的结构可以使用它们。这对静态列表或是仅是把结构初始化为某值而言相当有用。C++有构造函数,能够重复对象的初始化。但单单只有那样并不足以取代这项特色的所有机能。在C++03中,只允许在严格遵守POD的定义和限制条件的结构及类型上使用这项机能,非POD的类型不能使用,就连相当有用的STL容器std::vector也不行。

C++11将会把初始化列表的概念绑到类型上,称作std::initializer_list。这允许构造函数或其他函数像参数般地使用初始化列表。举例来说:

class SequenceClass
{
public:
  SequenceClass(std::initializer_list<int> list);
};

这将允许SequenceClass由一连串的整数构造,就像:

SequenceClass someVar = {1, 4, 5, 6};

这个构造函数是种特殊的构造函数,称作初始化列表构造函数。有着这种构造函数的类型在统一初始化的时候会被特别对待。

类型std::initializer_list<>是个第一级的C++11标准程序库类型。然而他们只能够经由C++11编译器通过{}语法的使用被静态地构造 。这个列表一经构造便可复制,虽然这只是copy-by-reference。初始化列表是常数;一旦被创建,其成员均不能被改变,成员中的数据也不能够被变动。

因为初始化列表是真实类型,除了类型构造式之外还能够被用在其他地方。正规的函数能够使用初始化列表作为引数。例如:

void FunctionName(std::initializer_list<float> list);
 
FunctionName({1.0f, -3.45f, -0.4f});

标准容器也能够以这种方式初始化:

vector<string> v = { "xyzzy", "plugh", "abracadabra" };

统一的初始化

标准 C++ 在初始化类型方面有着许多问题。初始化类型有数种方法,而且交换使用时不会都产生相同结果。传统的建构式语法,看起来像是函数声明,而且为了能使编译器不会弄错必须采取一些步骤。只有集合体和 POD 类型能够被集合式的初始化(使用SomeType var = {/*stuff*/};).

C++11 将会提供一种统一的语法初始化任意的对象,它扩充了初始化串行语法:

struct BasicStruct
{
 int x;
 float y;
};
 
struct AltStruct
{
  AltStruct(int _x, float _y) : x(_x), y(_y) {}
 
private:
  int x;
  float y;
};
 
BasicStruct var1{5, 3.2f};
AltStruct var2{2, 4.3f};

var1 的初始化的运作就如同 C-style 的初始化串行。每个公开的变量将被对应于初始化串行的值给初始化。隐式类型转换会在需要的时候被使用,这里的隐式类型转换不会产生范围缩限 (narrowing)。要是不能够转换,编译便会失败。(范围缩限 (narrowing):转换后的类型无法表示原类型。如将 32-bit 的整数转换为 16-bit 或 8-bit 整数,或是浮点数转换为整数。)var2 的初始化则是简单地调用建构式。

统一的初始化建构能够免除具体指定特定类型的必要:

struct IdString
{
  std::string name;
  int identifier;
};
 
IdString var3{"SomeName", 4};

该语法将会使用 const char * 参数初始化 std::string 。你也可以做像下面的事:

IdString GetString()
{
  return {"SomeName", 4}; // 注意這裡不需要明確的型別
}

统一初始化不会取代建构式语法。仍然会有需要用到建构式语法的时候。如果一个类型拥有初始化串行建构式(TypeName(initializer_list<SomeType>);),而初始化串行符合 sequence 建构式的类型,那么它比其他形式的建构式的优先权都来的高。C++11 版本的std::vector 将会有初始化串行建构式。这表示:

std::vector<int> theVec{4};

这将会调用初始化串行建构式,而不是调用std::vector只接受一个尺寸参数产生相应尺寸 vector 的建构式。要使用这个建构式,用户必须直接使用标准的建构式语法。

类型推导

在标准 C++(和 C ),使用变量必须明确的指出其类型。然而,随着模版类型的出现以及模板元编程的技巧,某物的类型,特别是函数定义明确的回返类型,就不容易表示。在这样的情况下,将中间结果存储于变量是件困难的事,可能会需要知道特定的元编程程序库的内部情况。

C++11 提供两种方法缓解上述所遇到的困难。首先,有被明确初始化的变量可以使用 auto 关键字。这会依据该初始化子(initializer)的具体类型产生变量:

auto someStrangeCallableType = boost::bind(&SomeFunction, _2, _1, someObject);
auto otherVariable = 5;

someStrangeCallableType 的类型就是模板函数 boost::bind 对特定引数所回返的类型。作为编译器语义分析责任的一部份,这个类型能够简单地被编译器决定,但用户要通过查看来判断类型就不是那么容易的一件事了。

otherVariable 的类型同样也是定义明确的,但用户很容易就能判别。它是个 int(整数),就和整数字面值的类型一样。

除此之外,decltype 能够被用来在编译期决定一个表示式的类型。举例:

int someInt;
decltype(someInt) otherIntegerVariable = 5;

decltypeauto 一起使用会更为有用,因为 auto 变量的类型只有编译器知道。然而 decltype 对于那些大量运用运算符重载和特化的类型的代码的表示也非常有用。

auto 对于减少冗赘的代码也很有用。举例而言,程序员不用写像下面这样:

for (vector<int>::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

而可以用更简短的

for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

这项差异随着程序员开始嵌套容器而更为显著,虽然在这种情况下 typedef 是一个减少代码的好方法。

decltype 所表示的类型可以和 auto 推导出来的不同。

#include <vector>
 
int main()
{
  const std::vector<int> v(1);
  auto a = v[0]// a 為 int 型別
  decltype(v[0]) b = 0;   // 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 實體的型別 
  decltype((c)) f = e; // f 為 int& 型別,因為(c)是左值
  decltype(0) g;      // g為int型別,因為0是右值
}

以范围为基础的 for 循环

Boost C++ 定义了许多"范围 (range) "的概念。范围表现有如受控制的串行 (list),持有容器中的两点。有序容器是范围概念的超集 (superset),有序容器中的两个迭代器 (iterator) 也能定义一个范围。这些概念以及操作的算法,将被并入 C++11 标准程序库。不过 C++11 将会以语言层次的支持来提供范围概念的效用。

for 述句将允许简单的范围迭代:

int my_array[5] = {1, 2, 3, 4, 5};
for (int &x : my_array)
{
  x *= 2;
}

上面 for 述句的第一部份定义被用来做范围迭代的变量,就像被声明在一般 for 循环的变量一样,其作用域仅只于循环的范围。而在":"之后的第二区块,代表将被迭代的范围。这样一来,就有了能够允许 C-style 数组被转换成范围概念的概念图。这可以是std::vector,或是其他符合范围概念的对象。

Lambda函数与表示式

在标准 C++,特别是当使用 C++ 标准程序库算法函数诸如 sortfind,用户经常希望能够在算法函数调用的附近定义一个临时的述部函数(又称谓词函数,predicate function)。由于语言本身允许在函数内部定义类型,可以考虑使用函数对象,然而这通常既麻烦又冗赘,也阻碍了代码的流程。此外,标准 C++ 不允许定义于函数内部的类型被用于模板,所以前述的作法是不可行的。

C++11 对
lambda
的支持可以解决上述问题。

一个 lambda 函数可以用如下的方式定义:

[](int x, int y) { return x + y; }

这个不具名函数的回返类型是 decltype(x+y)。只有在 lambda 函数符合"return expression"的形式下,它的回返类型才能被忽略。在前述的情况下,lambda 函数仅能为一个述句。

在一个更为复杂的例子中,回返类型可以被明确的指定如下:

[](int x, int y) -> int { int z = x + y; return z + x; }

本例中,一个暂时的变量 z 被创建用来存储中间结果。如同一般的函数,z 的值不会保留到下一次该不具名函数再次被调用时。

如果 lambda 函数没有传回值(例如 void ),其回返类型可被完全忽略。

定义在与 lambda 函数相同作用域的变量参考也可以被使用。这种的变量集合一般被称作 closure (闭包)。

[]  // 沒有定義任何變數。使用未定義變數會導致錯誤。
[x, &y] // x 以傳值方式傳入(預設),y 以傳參考方式傳入。
[&]   // 任何被使用到的外部變數皆隱式地以參考方式加以引用。
[=]   // 任何被使用到的外部變數皆隱式地以傳值方式加以引用。
[&, x]   // x 顯示地以傳值方式加以引用。其餘變數以參考方式加以引用。
[=, &z]   // z 顯示地以參考方式加以引用。其餘變數以傳值方式加以引用。

closure 被定义与使用如下:

std::vector<int> someList;
int total = 0;
std::for_each(someList.begin(), someList.end(), [&total](int x) {
  total += x;
});
std::cout << total;

上例可计算 someList 元素的总和并将其印出。 变量 total 是 lambda 函数 closure 的一部分,同时它以引用方式被传递入谓词函数, 因此它的值可被 lambda 函数改变。

若不使用引用的符号&,则代表变量以传值的方式传入 lambda 函数。 让用户可以用这种表示法明确区分变量传递的方法:传值,或是传参考。 由于 lambda 函数可以不在被声明的地方就地使用(如置入std::function 对象中); 这种情况下,若变量是以传参考的方式链接到 closure 中,是无意义甚至是危险的行为。

若 lambda 函数只在定义的作用域使用, 则可以用 [&] 声明 lambda 函数, 代表所有引用到 stack 中的变量,都是以参考的方式传入, 不必一一显式指明:

std::vector<int> someList;
int total = 0;
std::for_each(someList.begin(), someList.end(), [&](int x) {
  total += x;
});

变量传入 lambda 函数的方式可能随实做有所变化,一般期望的方法是 lambda 函数能保留其作用域函数的 stack 指针,借此访问区域变量。

若使用 [=] 而非 [&],则代表所有的参考的变量都是传值使用。

对于不同的变量,传值或传参考可以混和使用。 比方说,用户可以让所有的变量都以传参考的方式使用,但带有一个传值使用的变量:

int total = 0;
int value = 5;
[&, value](int x) { total += (x * value); };

total 是传参考的方式传入 lambda 函数,而 value 则是传值。

若一个 lambda 函数被定义于某类型的成员函数中,会被当作该类型的 friend。像这样的 lambda 函数可以使用该类型对象的参考,并且能够访问其内部的成员。

[](SomeType *typePtr) { typePtr->SomePrivateMemberFunction(); };

这只有当该 lambda 函数创建的作用域是在 SomeType 的成员函数内部时才能运作。

在成员函数中指涉对象的 this 指针,必须要显式的传入 lambda 函数, 否则成员函数中的 lambda 函数无法使用任何该对象的变量或函数。

[this]() { this->SomePrivateMemberFunction(); };

若是 lambda 函数使用 [&] 或是 [=] 的形式,this在 lambda 函数即为可见。

lambda 函数是编译器从属类型的函数对象; 这种类型名称只有编译器自己能够使用。如果用户希望将 lambda 函数作为参数传入,该类型必须是模版类型,或是必须创建一个std::function 去获取 lambda 的值。使用
auto 关键字让我们能够存储 lambda 函数:

auto myLambdaFunc = [this]() { this->SomePrivateMemberFunction(); };
auto myOnheapLambdaFunc = new auto([=] { /*...*/ });

但是,如果 lambda 函数是以参考的方式获取到它所有的 closure 变量,或者是没有 closure 变量,那么所产生的函数对象会被给予一个特殊的类型:std::reference_closure<R(P)>,其中 R(P) 是包含回返类型的函数签名。比起由
std::function 获取而来,这会是lambda函数更有效率的代表:

std::reference_closure<void()> myLambdaFunc = [this]() { this->SomePrivateMemberFunction(); };
myLambdaFunc();

另一种的函数语法

标准C 函数声明语法对于C语言已经足够。 演化自 C 的 C++ 除了 C 的基础语法外,又扩充额外的语法。 然而,当 C++ 变得更为复杂时,它暴露出许多语法上的限制, 特别是针对函数模板的声明。 下面的示例,不是合法的 C++03:

template< typename LHS, typename RHS> 
  Ret AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;} //Ret的型別必須是(lhs+rhs)的型別

Ret 的类型由 LHSRHS相加之后的结果的类型来决定。 即使使用 C++11 新加入的
decltype
来声明 AddingFunc 的返回类型,依然不可行。

template< typename LHS, typename RHS> 
  decltype(lhs+rhs) AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;} //不合法的 C++11

不合法的原因在于lhsrhs 在定义前就出现了。 直到剖析器解析到函数原型的后半部,lhs
rhs
才是有意义的。

针对此问题,C++11 引进一种新的函数定义与声明的语法:

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

这种语法也能套用到一般的函数定义与声明:

struct SomeStruct
{
auto

抱歉!评论已关闭.