Loading AI tools
2011版C ++編程語言標準 来自维基百科,自由的百科全书
C++11,先前被称作C++0x,即ISO/IEC 14882:2011,是C++程式语言的一个标准。它取代第二版标准ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998公开于1998年,第二版于2003年更新,分别通称C++98以及C++03,两者差异很小),且已被C++14取代。相比于C++03,C++11标准包含核心语言的新机能,而且扩展C++标准程式库,并入了大部分的C++ Technical Report 1程式库(数学的特殊函式除外)。 ISO/IEC JTC1/SC22/WG21 C++标准委员会计划在2010年8月之前完成对最终委员会草案的投票,以及于2011年3月召开的标准会议完成国际标准的最终草案。然而,WG21预期ISO将要花费六个月到一年的时间才能正式发布新的C++标准。为了能够如期完成,委员会决定致力于直至2006年为止的提案,忽略新的提案[1]。最终于2011年8月12日公布,并于2011年9月出版。
2012年2月28日的国际标准草案[1]是最接近于C++11标准的草案,差异仅有编辑上的修正。
像C++这样的程式语言,透过一种演化的过程来发展其定义。这个过程不可避免地将引发与现有程式码的相容问题,在C++的发展过程中偶尔会发生。不过根据比雅尼·斯特劳斯特鲁普(C++的创始人并且是委员会的一员)表示,新的标准将几乎100%相容于现有标准。
C++的修订包含核心语言以及标准程式库。
在发展新标准的每个机能上,委员会采取了几个方向:
关照初学者被认为是重要的,因为他们构成了计算机程序员的主体。也因为许多初学者不愿扩展他们对C++的知识,只限于使用他们对C++专精的部分。此外,考虑到C++被广泛的使用(包含应用领域和编程风格),即便是最有经验的程序员在面对新的编程范式时也会成为初学者。
C++委员会的主要作用之一是改善语言核心。核心语言将被大幅改善的领域包括多线程支援、泛型编程、统一的初始化,以及效能表现的加强。
在此分成4个区块来讨论核心语言的特色以及变更: 执行期表现强化、建构期表现强化、可用性强化,还有新的功能。某些特性可能会同时属于多个区块,但在此仅于其最具代表性的区块描述。
以下的语言机能主要用来提升某些效能表现,像是记忆体或是速度上的表现。
在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,把一个vector
的右值引用作为参数std::vector
的"move建构子",可以把右值参数所绑定的vector
内部的指向C-style阵列的指标复制给新的vector
,然后把该指标置null。由于临时变量不会被再次使用,所以不会有代码去访问该null指针;又因为该指针为null,当该临时对象超出作用域时曾经指向的内部C-style阵列所使用的内存不会被释放。因此,该操作不仅无形中免去了深拷贝的开销,而且还很安全。
右值引用作为数据类型的引入,使得函数可以重载区分它的参数是值类型、传统的左值引用还是右值引用。这让除了标准库的现有代码无须任何改动就能等到性能提升。一个返回std::vector<T>
的函数的返回类型无须为了调用move构造函数而显式修改为std::vector<T>&&
,因为临时对象自动作为右值。(但是,如果std::vector<T>
是没有move构造函数的C++03版,由于传统的左值引用也可以绑定到临时对象上,因此具有const std::vector<T>&
参数的复制构造函数会被调用,导致一次显著的内存分配。)
出于安全的考虑,推行了一些限制。具名的变量被认定为左值,即使它是被宣告为右值引用数据类型;为了获得右值必须使用显式类型转换,如模板函数std::move<T>()
。右值引用所绑定的对象应该只在特定情境下被修改,主要用于move构造函数中。
bool is_r_value(int &&) { return true; }
bool is_r_value(const int &) { return false; }
void test(int && i)
{
is_r_value(i); // i為具名變數,即使被宣告成右值引用类型,i作为实参表达式也不會被認定是右值表达式。
is_r_value(std::move<int&>(i)); // 使用std::move<T>()取得右值。
}
由于右值引用的语义特性以及对于左值引用(L-value references;regular references)的某些语义修正,右值引用让开发者能够提供函数参数的完美转发(perfect function forwarding)。当与不定长参数模板结合,这项能力允许函式模板能够完美地转送参数给其他接受这些特定参数的函式。最大的用处是转送建构子参数,创造出能够自动为这些特定参数呼叫正确建构式的工厂函式(factory function)。这个用法可以在C++标准库中的emplace_back(页面存档备份,存于互联网档案馆)方法中看到。
C++本来就已具备常数表示式(constant expression)的概念。像是3+4
总是会产生相同的结果并且没有任何的副作用。常数表示式对编译器来说是最佳化的机会,编译器时常在编译期执行它们并且将值存入程式中。同样地,在许多场合下,C++规格要求使用常数表示式。例如在阵列大小的定义上,以及列举值(enumerator values)都要求必须是常数表示式。
然而,常数表示式不能含有函式呼叫或是物件建构式。所以像是以下的例子是不合法的:
int GetFive() {return 5;}
int some_value[GetFive() + 7];// 欲產生12個整數的陣列。不合法的C++寫法
这在C++03中是不合法的,因为GetFive() + 7
并不是常数表示式。C++03编译器无从得知GetFive
实际上在执行期是常数。理论上而言,这个函式可能会影响全域变数,或者呼叫其他的非执行期(non-runtime)常数函式等。
C++11引进关键字constexpr
允许使用者保证函式或是物件建构式是编译期常数。以上的例子可以被写成像是下面这样:
constexpr int GetFive() {return 5;}
int some_value[GetFive() + 7];// 欲產生12個整數的陣列。合法的C++11寫法
这使得编译器能够了解并去验证GetFive
是个编译期常数。
用constexpr
修饰函数将限制函式的行为。首先,该函式的回返值型别不能为void。第二,函式的内容必须依照"return expr"的形式。第三,在参数替换后,expr
必须是个常数表示式。这些常数表示式只能够呼叫其他被定义为constexpr
的函式,或是其他常数表示式的资料变数。最后,有著这样修饰符的函式直到在该编译单元内被定义之前是不能够被呼叫的。
声明为constexpr
的函数也可以像其他函数一样用于常量表达式以外的地方,此时不需要满足后两点。
C++11之前,可以在常量表达式中使用的变量必须被声明为const
,用常量表达式来初始化,并且必须是整型或枚举类型。C++11去除了变量必须是整型或枚举类型的限制,只要变量使用了constexpr
关键字来定义:
constexpr double earth_gravitational_acceleration = 9.8;
constexpr double moon_gravitational_acceleration = earth_gravitational_acceleration / 6.0;
这些变量都是隐式常量,必须使用常量表达式来初始化。
为了让使用者自定义型别(user-defined type)参与建构常量表示式,建构式也可以用constexpr
来声明。与constexpr
函式一样,constexpr
建构式必须在该编译单元内使用之前被定义。它的函数体必须为空。它必须用常量表示式初始化他的成员(member)。而这种型别的解构式应当是平凡的(trivial)。
拥有constexpr
构造函数的类型的复制构造函数通常也应该被定义为constexpr
,以便该类型的对象以值传递的方式从constexpr
函数返回。该类别的任何成员函式,像是复制建构式、运算符重载函数等等,只要他们符合常数表达式函式的定义,都可以被宣告成constexpr
。使得编译器能够在编译期进行类别的复制、对他们施行运算等等。
常数表达式函式或建构式,可以以非常数表示式(non-constexpr
)作为参数唤起。就如同constexpr
整数字面值能够指派给non-constexpr
变数,constexpr
函式也可以接受non-constexpr
参数,其结果储存于non-constexpr
变数。constexpr
关键字只有当表示式的成员都是constexpr
,才允许编译期常数性的可能。
在C++03中,一个类(class)或结构(struct)要想被作为POD,必须遵守几条规则。符合这种定义的型别能够产生与C相容的物件内存布局(object layout),而且可以被静态初始化。但C++03标准严格限制了何种类型与C兼容或可以被静态初始化的,尽管并不存在技术原因导致编译器无法处理。如果创建一个C++03 POD类型,然后为其添加一个非虚成员函数,这个类型就不再是POD类型了,从而无法被静态初始化,也不再与C兼容,尽管其内存布局并没有发生变化。
C++11通过把POD概念划分成两个概念:平凡的(trivial)和标准布局(standard-layout),放宽了关于POD的定义。
一个平凡的类型可以被静态初始化,意味着使用memcpy
来复制数据是合法的,而无须使用复制构造函数。平凡的类型对象的生命周期开始于其存储空间被分配时,而不是其构造函数完成时。使用模版类std::is_trivial<T>::value来判断数据类型是否为平凡类型
一个平凡的类别或结构符合以下定义:
SomeConstructor() = default;
一个符合标准布局的类封装成员的方式与C兼容。使用模版类std::is_standard_layout<A>::value来判断类型是否是一个标准布局类型。一个标准布局(standard-layout)的类别或结构符合以下定义:
一个类、结构、联合只有在其是平凡的、符合标准布局,并且所有非静态成员和基类都是POD时,才被视为POD。使用<type_traits>中的is_pod<T>::value判断T是不是POD类型
通过划分,使得放弃一个特性而不失去另一个成为可能。一个具有复杂的复制和move构造函数的类可能不是平凡的,但是它可能符合标准布局,从而能与C程序交互。类似地,一个同时具有public和private数据成员的类不符合标准布局,但它可以是平凡的,从而能够使用memcpy
来复制。
在标准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" };
vector<string> v({ "xyzzy", "plugh", "abracadabra" });
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>);),而初始化串列与构造函数的参数类型一致,那么它比其他形式的建构式的优先权都来的高。C++11版本的std::vector将会有初始化串列建构式。这表示:
std::vector<int> theVec{4};
这将会呼叫初始化串列建构式,而不是呼叫std::vector只接受一个尺寸参数产生相应尺寸vector的建构式。要使用这个建构式,使用者必须直接使用标准的建构式语法。
在C++03和C,使用变数必须明确的指出其型别。然而,随著模版型别的出现以及模板超编程的技巧,某物的型别,特别是函式定义明确的回返型别,就不容易表示。在这样的情况下,将中间结果储存于变数是件困难的事,可能会需要知道特定的超编程程式库的内部情况。
C++11提供两种方法缓解上述所遇到的困难。首先,有被明确初始化的变数可以使用auto
关键字。对于指针类型,声明为auto* 或者auto 是一样的。对于引用类型,必须使用auto & 。这会依据该初始化子(initializer)的具体型别产生变数:
auto integralVariable = 5;
auto unsignedVariable = 5UL;
auto ptrToObject = new MyPackage::Object();
在上面的简易例子中,程序员和编译器都能轻易判断出几个变量的类型。而通过使用UL
后缀,unsignedVariable
的类型自动成为unsigned long
。最后,动态创建对象时已经要在 new
后输入类型,此时使用 auto
就能省略掉累赘的MyPackage::Object*
声明。
using namespace std::placeholders;
auto someStrangeCallableType = std::bind(&SomeFunction, _2, _1, someObject);
auto otherVariable = 5;
上面的例子中说明auto
有利于C++支援函数式编程。这里注意std::bind
也是C++11从Boost C++ Libraries中引入的模版函数。std::bind
实现了偏函数,在上面的例子中它将函数SomeFunction
的第三个参数绑定为someObject
,并将第一和第二个参数的在参数列表中的顺序倒转,由此生成一个函数对象someStrangeCallableType
。该函数对象的类型非常复杂,但编译器却能轻易地将其推导出来。
除此之外,decltype
能够被用来在编译期决定一个表示式的型别。举例:
int someInt;
decltype(someInt) otherIntegerVariable = 5;
decltype
和auto
一起使用会更为有用,因为auto变数的型别只有编译器知道。然而decltype
对于那些大量运用运算子重载和特化的型别的程式码的表示也非常有用。
auto
对于减少冗赘的程式码也很有用。举例而言,程式员不用写像下面这样:
for (vector<int>::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)
而可以用更简短的
for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)
由于"myvec"实现了begin/end迭代器,C++11提供了基于范围的for循环来大幅度省略代码。
for (auto& x : myvec)
这项差异随著程式员开始嵌套容器而更为显著,虽然在这种情况下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语句将允许简单的范围迭代:
int my_array[5] = {1, 2, 3, 4, 5};
// double the value of each element in my_array:
for (int &x : my_array)
{
x *= 2;
}
// similar but also using type inference for array elements
for (auto &x : my_array) {
x *= 2;
}
上面for述句的第一部份定义被用来做范围迭代的变量,就像被宣告在一般for回圈的变量一样,其作用域仅只于回圈的范围。而在":"之后的第二区块,代表将被迭代的范围。这种for语句还可以用于C型数组,初始化列表,和任何定义了begin()
和end()
来返回首尾迭代器的类型。
在标准C++,特别是当使用C++标准程式库演算法函式诸如sort和find,使用者经常希望能够在演算法函式呼叫的附近定义一个临时的述部函式(又称谓词函数,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函式被定义于某类别的成员函式中,则可以使用该类别物件的参考,并且能够存取其内部的成员。
[](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函数按照值方式捕获的环境中的变量,是不能修改的。否则,编译器会报错:“by copy capture cannot be modified in a non-mutable lambda”。其值是lambda函数定义时捕获的值,不再改变。如果在lambda函数定义时加上mutable关键字,则该捕获的传值变量是可以修改的,对同一个lambda函数的随后调用也会累加影响该捕获的传值变量,但对外界环境中被捕获的那个变量无影响。例如:
#include <iostream>
using namespace std;
int main()
{
size_t t = 9;
auto f = [t]() mutable {return t; };
cout << f() << endl;
t = 100;
cout << f() << endl;
cout << "t:" << t << endl;
return 0;
}
标准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的型别由LHS与RHS相加之后的结果的型别来决定。即使使用C++11新加入的decltype来宣告AddingFunc的返回型别,依然不可行。
template< typename LHS, typename RHS>
decltype(lhs+rhs) AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;} //不合法的C++11
不合法的原因在于lhs及rhs在定义前就出现了。直到剖析器解析到函数原型的后半部,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 FuncName(int x, int y) -> int;
};
auto SomeStruct::FuncName(int x, int y) -> int
{
return x + y;
}
关键字auto的使用与其在自动型别推导代表不同的意义。
在C++03中,建构式不能呼叫其它的建构式;每个建构式必须自己初始化所有的成员或是呼叫一个共用的成员函式。基础类别的建构式不能够直接作为衍生类别的建构式;就算基类的建构式已经足够,每个衍伸的类别仍必须实做自己的建构式。类别中non-constant的资料成员不能够在宣告的地方被初始化,它们只能在建构式中被初始化。 C++11将会提供这些问题的解决方案。
C++11允许建构式呼叫其他建构式,这种做法称作委托(delegation)构造。仅仅只需要加入少量的代码,就能让数个建构式之间达成功能复用(reuse)。Java以及C♯都有提供这种功能。C++11语法如下:
class SomeType {
int number;
string name;
SomeType( int i, string& s ) : number(i), name(s){}
public:
SomeType( ) : SomeType( 0, "invalid" ){}
SomeType( int i ) : SomeType( i, "guest" ){}
SomeType( string& s ) : SomeType( 1, s ){ PostInit(); }
};
class DCExcept{
public:
DCExcept(double d)
try : DCExcept(1, d){
cout << "run the body." << endl;
}catch(...){
cout << "caught exception."<<endl;
}
private:
DCExcept(int i, double d){
cout << "going to throw" << endl;
throw 0;
}
int type;
double date;
};
C++03中,建构式执行结束代表物件建构完成; 而允许使用转接建构式的C++11则是以"任何"一个建构式结束代表建构完成。使用委托的建构式,函式本体中的代码将于被委托的建构式完成后继续执行(如上例的PostInit())。若基类使用了委托建构式,则衍生类别的建构式会在"所有"基底类别的建构式都完成后,才会开始执行。
C++11允许衍生类别手动继承基底类别的建构式,编译器可以使用基底类别的建构式完成衍生类别的建构。而将基类的建构式带入衍生类的动作,无法选择性地部分带入,要不就是继承基类全部的建构式,要不就是一个都不继承(不手动带入)。此外,若牵涉到多重继承,从多个基底类别继承而来的建构式不可以有相同的函式签名(signature)。而衍生类别的新加入的建构式也不可以和继承而来的基底建构式有相同的函式签名,因为这相当于重复宣告。
语法如下:
class BaseClass
{
public:
BaseClass(int iValue);
};
class DerivedClass : public BaseClass
{
public:
using BaseClass::BaseClass;
};
此语法等同于DerivedClass宣告一个DerivedClass(int)的建构式。同时也因为DerivedClass有了一个继承而来的建构式,所以不会有预设建构式。
另一方面,C++11可以使用以下的语法完成数据成员的原地(in-place)初始化:
class SomeClass
{
public:
SomeClass() {}
explicit SomeClass(int iNewValue) : iValue(iNewValue) {}
private:
int iValue = 5;
};
若是建构式中没有设定iValue的初始值,则会采用类别定义中的成员初始化,令iValue初值为5。在上例中,无参数版本的建构式,iValue便采用预设所定义的值;而带有一个整数参数的建构式则会以指定的值完成初始化。
成员初始化除了上例中的赋值形式(使用"="),也可以采用建构式以及统一形的初始化(uniform initialization,使用"{}")。
在C++里,在子类别中容易意外的重载虚函数。举例来说:
struct Base {
virtual void some_func();
};
struct Derived : Base {
void some_func();
};
Derived::some_func
的真实意图为何?程序员真的试图重载该虚函数,或这只是意外?这也可能是base
的维护者在其中加入了一个与Derived::some_func
同名且拥有相同签名的虚函式。
另一个可能的状况是,当基类中的虚函式的签名被改变,子类中拥有旧签名的函式就不再重载该虚函式。因此,如果程序员忘记修改所有子类,执行期将不会正确呼叫到该虚函式正确的实现。
C++11将加入支援用来防止上述情形产生,并在编译期而非执行期捕获此类错误。为保持向后兼容,此功能将是选择性的。其语法如下:
struct Base {
virtual void some_func(float);
};
struct Derived : Base {
virtual void some_func(int) override; // 錯誤格式:Derive::some_func並沒有override Base::some_func
virtual void some_func(float) override; // OK:顯式改寫
};
编译器会检查基底类别是否存在一虚拟函数,与衍生类别中带有声明override
的虚拟函数,有相同的函数签名(signature);若不存在,则会回报错误。
C++11也提供指示字final
,用来避免类别被继承,或是基底类别的函数被改写:
struct Base1 final { };
struct Derived1 : Base1 { }; // 錯誤格式:class Base1已標明為final
struct Base2 {
virtual void f() final;
};
struct Derived2 : Base2 {
void f(); // 錯誤格式:Base2::f已標明為final
};
以上的范例中,virtual void f() final;
声明一新的虚拟函数,同时也表明禁止衍生函数改写原虚拟函数。
override
与final
都不是语言关键字(keyword),只有在特定的位置才有特别含意,其他地方仍旧可以作为一般指示字(identifier)使用。
早在1972年,C语言诞生的初期,常数0带有常数及空指标的双重身分。
C使用preprocessor macro NULL
表示空指标,让NULL
及0
分别代表空指标及常数0。
NULL
可被定义为((void*)0)
或是0
。
C++并不采用C的规则,不允许将void*
隐式转换为其他型别的指标。为了使代码char* c = NULL;
能通过编译,NULL只能定义为0
。这样的决定使得函数多载无法区分代码的语意:
void foo(char *);
void foo(int);
void foo(nullptr_t);
C++建议NULL
应当定义为0
,所以foo(NULL);
将会呼叫foo(int)
,这并不是程序员想要的行为,也违反了代码的直观性。0的歧义在此处造成困扰。
C++11引入了新的关键字来代表空指标常数:nullptr
,将空指标和整数0的概念拆开。
nullptr
的型别为nullptr_t
,能隐式转换为任何指标或是成员指标的型别,也能和它们进行相等或不等的比较。而nullptr
不能隐式转换为整数,也不能和整数做比较。
为了向下相容,0
仍可代表空指标常数。
char* pc = nullptr; // OK
int * pi = nullptr; // OK
int i = nullptr; // error
bool b = nullptr; // OK
foo(pc); // 调用foo(char *), 而不是 foo(int);
foo(nullptr); // 调用foo(nullptr_t);
值得注意的是上面的 foo(nullptr_t) 被隐式转换为 foo(char *) 只会发生在该函数不存在其它的指针类型重载(比如 foo(int*), foo(MyClass*)等)时候,否则就会产生歧义错误(可以通过显示声明一个 foo(nullptr_t) 来消除该歧义)。 在C++11的标准类型头文件中,nullptr_t 类型应该被声明为:
typedef decltype(nullptr) nullptr_t;
而不是:
typedef int nullptr_t; // C++11之前的标准需要定义 NULL 为 0
typedef void *nullptr_t; // ANSI C 定义 NULL 为 ((void*)0)
在C++03中,列举型别不是型别安全的。列举型别被视为整数,这使得两种不同的列举型别之间可以进行比较。C++03唯一提供的安全机制是一个整数或一个枚举型值不能隐式转换到另一个列举别型。此外,列举所使用整数型别及其大小都由实作方法定义,皆无法明确指定。最后,列举的名称全数暴露于枚举类型的作用域中,因此两个不同的列举,不可以有相同的列举名。(好比 enum Side{ Right, Left }; 和 enum Thing{ Wrong, Right }; 不能一起使用。)
C++11引进了一种特别的"列举类",可以避免上述的问题。使用enum class的语法来宣告:
enum class myEnumeration
{
Val1,
Val2,
Val3 = 100,
Val4 /* = 101 */,
};
此种列举为型别安全的。列举类别不能隐式地转换为整数;也无法与整数数值做比较。(表示式Enumeration::Val4 == 101
会触发编译期错误)。
列举类别所使用型别必须显式指定。在上面的范例中,使用的是预设型别int,但也可以指定其他型别:
enum class Enum2 : unsigned int {Val1, Val2};
列举类别的作用域(scoping)不包含枚举值的名字。使用枚举值的名字,必须明确限定于其所属的枚举类型。例如,前述列举类别Enum2,Enum2::Val1是有意义的表示法,而单独的Val1则否。
此外,C++11允许为传统的列举指定使用型别:
enum Enum3 : unsigned long {Val1 = 1, Val2};
列举名Val1定义于Enum3的列举范围中(Enum3::Val1),但为了向后相容性, Val1仍然可以于所属枚举类型所在的作用域中单独使用。
在C++11中,列举类别的前置声明(forward declaration)也是可行的,只要使用可指定型别的新式列举即可。之前的C++无法写出列举的前置声明,是由于无法确定列举变数所占的空间大小,C++11解决了这个问题:
enum Enum1; // C++與C++11中不合法;無法判別大小
enum Enum2 : unsigned int; // 合法的C++11
enum class Enum3; // 合法的C++11,列舉類別使用預設型別int
enum class Enum4: unsigned int; // 合法的C++11
enum Enum2 : unsigned short; // 不合法的C++11,Enum2已被聲明為unsigned int
C++03的剖析器一律将">>"视为右移运算子。但在嵌套样板定义式中,绝大多数的场合其实都代表两个连续右角括号。为了避免剖析器误判,撰码时不能把右角括号连著写。
C++11变更了剖析器的解读规则;当遇到连续的右角括号时,会在合理的情况下将右尖括号解析为样板引数的结束符号。给使用>
,>=
,>>
的表达式加上圆括号,可以避免其与圆括号外部的左尖括号相匹配:
template<bool bTest> class SomeType;
std::vector<SomeType<1>2>> x1; // 解讀為std::vector of "SomeType<true> 2>",
// 非法的表示式,整數1被轉換為bool型別true
std::vector<SomeType<(1>2)>> x1; // 解讀為std::vector of "SomeType<false>",
// 合法的C++11表示式,(1>2)被轉換為bool型別false
C++98引入了关键字explicit来避免用户自定的单引数建构式被当成隐式型别转换子。但是,却没有限制明确定义的类型转换函数。比方说,一个smart pointer类别具有一个operator bool(),被定义成若该smart pointer不为null则传回true,反之传回false。遇到这样的代码时:if(smart_ptr_variable),编译器可以借由operator bool()隐式转换成布林值,和测试原生指标的方法一样。但是这类隐式转换同样也会发生在非预期之处。由于C++的bool型别也是算术型别,能隐式换为整数甚至是浮点数。拿物件转换出的布林值做布林运算以外的数学运算,往往不是程序员想要的。
在C++11中,关键字explicit修饰符也能套用到型别转换函数上。如同建构式一样,它能避免型别转换函数被隐式转换调用。但C++11特别指定,在if条件式、回圈、逻辑运算等需要布林值的地方,将其作为显式类型转换,因此即使对应的类型转换函数被explicit修饰也可以调用。这主要为了解决safe bool(页面存档备份,存于互联网档案馆)问题。
在进入这个主题之前,各位应该先弄清楚“模板”和“型别”本质上的不同。class template (类别模板,是模板)是用来产生template class(模板类别,是型别)。在C++03中,typedef
可定义模板类别一个新的型别名称,但是不能够使用typedef
来定义模板的别名。举例来说:
template< typename first, typename second, int third>
class SomeType;
template< typename second>
typedef SomeType<OtherType, second, 5> TypedefName; // 在C++03是不合法的
这不能够通过编译。
为了定义模板的别名,C++11将会增加以下的语法:
template< typename first, typename second, int third>
class SomeType;
template< typename second>
using TypedefName = SomeType<OtherType, second, 5>;
using也能在C++11中定义一般型别的别名,等同typedef:
typedef void (*PFD)(double); // 傳統語法
using PFD = void (*)(double); // 新增語法
C++98支持类模板的模板参数默认值,不支持函数模板的模板参数默认值。C++11可以支持函数模板的模板参数默认值。类模板的模板参数默认值需要从右到左依次出现;函数模板的默认模板参数没有此约束。
void DefParm(int m = 3) {} // c++98编译通过,c++11编译通过
template <typename T = int>
class DefClass {}; // c++98编译通过,c++11编译通过
template <typename T = int>
void DefTempParm() {}; // c++98编译失败,c++11编译通过
//以下为C++11编译:
template <typename T1, typename T2 = int>
class DefClass1 {};
template <typename T1 = int, typename T2>
class DefClass2 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则
template <typename T, int i = 0>
class DefClass3 {};
template <int i = 0, typename T>
class DefClass4 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则
template <typename T1 = int, typename T2>
void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则
template <int i = 0, typename T>
void DefFunc2(T a) {}; // OK 函数模板不用遵循“由右往左”的规则
//通常,如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用:
template <class T, class U = double>
void f(T t = 0, U u = 0) {};
void g()
{
f(1, 'c'); // f<int, char>(1, 'c')
f(1); // f<int, double>(1, 0), 使用了默认模板参数double
f(); // 错误: T无法被推导出来
f<int>(); // f<int, double>(0, 0), 使用了默认模板参数double
f<int, char>(); // f<int, char>(0, 0)
}
在C++03中,并非任意的型别都能做为union的成员。比方说,带有non-trivial 建构式的型别就不能是union的成员。在新的标准里,移除了所有对union的使用限制,除了其成员仍然不能是引用型别。这一改变使得union更强大,更有用,也易于使用。[2]
但是如果union成员具有非平凡的特殊成员函数(页面存档备份,存于互联网档案馆),则编译器不会为union生成对应的特殊成员函数,必须手工定义。
以下为C++11中union使用的简单范例:
struct Point
{
Point() {}
Point(int x, int y): x_(x), y_(y) {}
int x_, y_;
};
union U
{
int z;
double w;
Point p; // 在C++03中是不合法(point有一non-trivial建構式),但是在C++11是合法的
U() {} // 由于 Point 成员的存在,必须要定义一个构造函数
U(const Point& pt) : p(pt) {} // 通过初始化列表构造 Point 对象
U& operator=(const Point& pt) { new (&p) Point(pt); return *this; } // 通过原地new方式赋值构造Point对象
};
这一改变仅放宽union的使用限制,不会影响既有的旧代码。
这些特性让C++语言能够做一些以前做不到的,或者极其复杂的,或者需求一些不可移植的库的事情。
在C++11之前,不论是类模板或是函式模板,都只能按其被声明时所指定的样子,接受一组固定数目的模板参数;C++11加入新的表示法,允许任意长度、任意型别的模板参数,不必在定义时将参数的个数固定。
template<typename... Values> class tuple;
模板类tuple的物件,能接受不限个数的typename作为它的模板引数:
class tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> someInstanceName;
实参的个数也可以是0,所以class tuple<> someInstanceName这样的定义也是可以的。
若不希望产生实参个数为0的不定长参数模板,则可以采用以下的定义:
template<typename First, typename... Rest> class tuple;
不定长参数模板也能运用到模板函式上。传统C中的printf函式,虽然也能达成不定长度的引数的调用,但其并非型别安全。以下的范例中,C++11除了能定义型别安全的不定长引数函式外,还能让类似printf的函式能自然地处理非内建型别的物件。除了在模板参数中能使用...表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数。
template<typename... Params> void printf(const std::string &strFormat, Params... parameters);
其中,Params与parameters分别代表模板与函式的不定长参数集合,称之为参数包(parameter pack)。参数包必须要和算子"..."搭配使用,避免语法上的歧义。
不定长参数模板中,不定长参数包无法如同一般参数在类或函式中使用; 因此典型的手法是以递回的方法取出可用参数,参看以下的C++11 printf范例:
void printf(const char *s)
{
while (*s)
{
if (*s == '%' && *(++s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char* s, T value, Args... args)
{
while (*s)
{
if (*s == '%' && *(++s) != '%')
{
std::cout << value;
printf(*s ? ++s : s, args...); // 即便当*s == 0也会产生调用,以检测更多的类型参数。
return;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
printf会不断地递回调用自身:函式参数包args...在调用时,会被模板型别匹配分离为T value和Args... args。直到args...变为空参数,则会与简单的printf(const char *s)形成匹配,结束递回。
另一个例子为计算模板参数的长度,这里使用相似的技巧展开模板参数包Args...:
template<typename... args>
struct Count{};
template<>
struct count<> {
static const int value = 0;
};
template<typename T, typename... Args>
struct count<T, Args...> {
static const int value = 1 + count<Args...>::value;
};
虽然没有一个简洁的机制能够对变长参数模板中的值进行迭代,但使用算子"..."还能在代码各处对参数包施以更复杂的展开操作。举例来说,一个模板类的定义:
template <typename... BaseClasses> class ClassName : public BaseClasses...
{
public:
ClassName (BaseClasses&&... baseClasses) : BaseClasses(baseClasses)... {}
}
BaseClasses...会被展开成类别ClassName的基底类;ClassName的建构式需要所有基底类的右值引用,而每一个基底类都是以传入的参数做初始化(BaseClasses(baseClasses)...)。
在函式模板中,不定长参数可以和右值参照搭配,达成引数的完美转送(perfect forwarding):
template<typename TypeToConstruct> struct SharedPtrAllocator
{
template<typename... Args> std::shared_ptr<TypeToConstruct> ConstructWithSharedPtr(Args&&... params)
{
return std::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...));
}
}
参数包parms可展开为TypeToConstruct建构式的引数。表示式std::forward<Args>(params)可将引数的型别信息保留(利用右值参照),传入建构式。而算子"..."则能将前述的表示式套用到每一个参数包中的参数。这种工厂函式(factory function)的手法,使用std::shared_ptr管理配置物件的记忆体,避免了不当使用所产生的记忆体泄漏(memory leaks)。
此外,不定长参数的数量可以藉以下的语法得知:
template<typename ...Args> struct SomeStruct
{
static const int size = sizeof...(Args);
}
SomeStruct<Type1, Type2>::size是2,而SomeStruct<>::size会是0。(sizeof...(Args)的结果是编译期常数。)
标准C++提供了两种字串字面值。第一种,包含有双引号,产生以空字元结尾的const char阵列。第二种有著前标L,产生以空字元结尾的const wchar_t阵列,其中wchar_t代表宽字元。对于Unicode编码的支援尚付阙如。
为了加强C++编译器对Unicode的支援,型别char的定义被修改为其大小至少能够储存UTF-8的8位元编码,并且能够容纳编译器的基本字元集的任何成员。
C++11将支援三种Unicode编码方式:UTF-8,UTF-16,和UTF-32。除了上述char定义的变更,C++11将增加两种新的字元型别:char16_t和char32_t。它们各自被设计用来储存UTF-16以及UTF-32的字元。
以下展示如何产生使用这些编码的字串字面值:
u8"I'm a UTF-8 string."
u"This is a UTF-16 string."
U"This is a UTF-32 string."
第一个字串的型别是通常的const char[]
;第二个字串的型别是const char16_t[]
;第三个字串的型别是const char32_t[]
。
当建立Unicode字串字面值时,可以直接在字串内插入Unicode codepoints。C++11提供了以下的语法:
u8"This is a Unicode Character: \u2018."
u"This is a bigger Unicode Character: \u2018."
U"This is a Unicode Character: \U00002018."
在'\u'之后的是16个位元的十六进位数值;它不需要'0x'的前标。识别字'\u'代表了一个16位元的Unicode codepoint;如果要输入32位元的codepoint,使用'\U'和32个位元的十六进位数值。只有有效的Unicode codepoints能够被输入。举例而言,codepoints在范围U+D800—U+DFFF之间是被禁止的,它们被保留给UTF-16编码的surrogate pairs。
有时候避免手动将字串换码也是很有用的,特别是在使用XML档案或是一些脚本语言的字面值的时候。C++11将提供raw(原始)字串字面值:
R"(The String Data \ Stuff " )" R"delimiter(The String Data \ Stuff " )delimiter"
在第一个例子中,任何包含在(
)
括号(标准已经从[]
改为()
)当中的都是字串的一部分。其中"
和\
字元不需要经过转义。在第二个例子中,"delimiter(
开始字串,只有在遇到)delimiter"
才代表结束。其中delimiter
可以是最多16个字符的任意的字串(包含空字符串),但不能包含空格、控制字符和'('、')'、'\'。原始字符串允许使用者使用圆括号(
,)
,例如R"delimiter((a-z))delimiter"等价于"(a-z)"。原始字串字面值能够和宽字面值或是Unicode字面值结合:
u8R"XXX(I'm a "raw UTF-8" string.)XXX"
// uR"*@(This is a "raw UTF-16" string.)*@" //visual studio not support '@'
uR"*(This is a "raw UTF-16" string.)*"
UR"(This is a "raw UTF-32" string.)"
标准C++提供了数种字面值。字元"12.5"是能够被编译器解释为数值12.5的double型别字面值。然而,加上"f"的后置,像是"12.5f",则会产生数值为12.5的float型别字面值。之前的C++规范中字面值的修饰符是固定的,C++代码不能创立新的字面修饰符。
C++11开放使用者定义新的字面修饰符(literal modifier),利用自订的修饰符完成由字面值建构物件。
字面值转换可以定义为两个阶段:原始与转换后(raw与cooked)。原始字面值指特定类型的字符序列,而转换后的字面值则代表另一种型别。如字面值1234,原始字面值是'1', '2', '3', '4'的字符序列;而转换后的字面值是整数值1234。另外,字面值0xA转换前是序列'0', 'x', 'A';转换后代表整数值10。
C++标准委员会计划统一对多绪编程的支援。
这将涉及两个部分:第一、设计一个可以使多个线程在一个进程中共存的内存模型;第二、为线程之间的互动提供支援。第二部分将由程式库提供支持,更多请看绪程支援。
在多个线程可能会访问相同内存的情形下,由一个内存模型对它们进行调度是非常有必要的。遵守模型规则的程序是被保证正确运行的,但违反规则的程序会发生不可预料的行为,这些行为依赖于编译器的最佳化和记忆体一致性的问题。
在多绪环境下,让各绪程拥有各自的变数是很普遍的。这已经存在于函式的区域变数,但是对于全域和静态变数都还不行。
新的thread_local存储期限(在现行的static、dynamic和automatic之外)被作为下个标准而提出。绪程区域的存储期限会借由存储指定字thread_local
来表明。
static物件(生命周期为整个程式的执行期间)的存储期限可以被thread-local给替代。就如同其他使用static存储期的变数,thread-local物件能够以建构式初始化并以解构式摧毁。
在传统C++中,若使用者没有提供,则编译器会自动为物件生成预设建构式(default constructor)、复制建构式(copy constructor),赋值运算子(copy assignment operator operator=)以及解构式(destructor)。另外,C++也为所有的类别定义了数个全域算子(如operator delete及operator new)。当使用者有需要时,也可以提供自订的版本改写上述的函式。
问题在于原先的c++无法精确地控制这些预设函数的生成。比方说,要让类别不能被拷贝,必须将复制建构式与赋值运算子宣告为private,并不去定义它们。尝试使用这些未定义的函式会导致编译期或连结期的错误。但这种手法并不是一个理想的解决方案。
此外,编译器产生的预设建构式与使用者定义的建构式无法同时存在。若使用者定义了任何建构式,编译器便不会生成预设建构式; 但有时同时带有上述两者提供的建构式也是很有用的。目前并没有显式指定编译器产生预设建构式的方法。
C++11允许显式地表明采用或拒用编译器提供的内建函式。例如要求类别带有预设建构式,可以用以下的语法:
struct SomeType
{
SomeType() = default; // 預設建構式的顯式聲明
SomeType(OtherType value);
};
另一方面,也可以禁止编译器自动产生某些函式。如下面的例子,类别不可复制:
struct NonCopyable
{
NonCopyable & operator=(const NonCopyable&) = delete;
NonCopyable(const NonCopyable&) = delete;
NonCopyable() = default;
};
禁止类别以operator new配置记忆体:
struct NonNewable
{
void *operator new(std::size_t) = delete;
};
此种物件只能生成于stack中或是当作其他类别的成员,它无法直接配置于heap之中,除非使用了与平台相关,不可移植的手法。(使用placement new算子虽然可以在用户自配置的记忆体上呼叫物件建构式,但在此例中其他形式的new算子一并被上述的定义遮蔽("name hiding"),所以也不可行。)
= delete
的声明(同时也是定义)也能适用于非内建函式,禁止成员函式以特定的引数呼叫:
struct NoDouble
{
void f(int i);
void f(double) = delete;
};
若尝试以double的引数呼叫f()
,将会引发编译期错误,编译器不会自动将double引数转型为int再呼叫f()
。若要彻底的禁止以非int的引数呼叫f()
,可以将= delete
与模板相结合:
struct OnlyInt
{
void f(int i);
template<class T> void f(T) = delete;
};
在32位元系统上,一个long long int
是保有至少64个有效位元的整数型别。C99将这个型别引入了标准C中,目前大多数的C++编译器也支援这种型别。C++11将把这种型别添加到标准C++中。
C++提供了两种方法测试assertion(声明):巨集assert
以及前处理器指令#error
。但是这两者对于模版来说都不合用。巨集在执行期测试assertion,而前处理器指令则在前置处理时测试assertion,这时候模版还未能具现化。所以它们都不适合来测试牵扯到模板参数的相关特性。
新的机能会引进新的方式可以在编译期测试assertion,只要使用新的关键字static_assert
。宣告采取以下的形式:
static_assert( constant-expression, error-message ) ;
这里有一些如何使用static_assert
的例子:
static_assert( 3.14 < GREEKPI && GREEKPI < 3.15, "GREEKPI is inaccurate!" ) ;
template< class T >
struct Check
{
static_assert( sizeof(int) <= sizeof(T), "T is not big enough!" ) ;
} ;
当常数表示式值为false
时,编译器会产生相应的错误讯息。第一个例子是前处理器指令#error
的替代方案;第二个例子会在每个模板类别Check
生成时检查assertion。
静态assertion在模板之外也是相当有用的。例如,某个演算法的实作依赖于long long
型别的大小比int
还大,这是标准所不保证的。这种假设在大多数的系统以及编译器上是有效的,但不是全部。
C++98, sizeof只能对实例的变量或者类的静态成员进行操作,不能对类的非静态成员进行操作,若要想达成对类的非静态成员的操作,可以用如下ugly方式, 0强转成对象的指针,并解析访问对应非静态成员变量。
struct SomeType { OtherType member; };
sizeof( (SomeType *)(0) ->member); //C++98
sizeof(SomeType::member); // 直接由SomeType型別取得非靜態成員的大小,C++03不行。C++11允許
这会传回OtherType的大小。C++03并不允许这样做,所以会引发编译错误。C++11将会允许这种使用。
是否会自动回收那些无法被使用到(unreachable)的动态分配物件由实作决定。
参见<memory>头文件中的几个函数:
C++11标准程式库有数个新机能。其中许多可以在现行标准下实作,而另外一些则依赖于(或多或少)新的C++11核心语言机能。
新的程式库的大部分被定义于C++标准委员会的Library Technical Report(称TR1),于2005年发布。各式TR1的完全或部分实作目前提供在命名空间std::tr1
。C++11会将其移置于命名空间std
之下。
目前的标准库能受益于C++11新增的一些语言特性。举例来说,对于大部份的标准库容器而言,像是搬移内含大量元素的容器,或是容器之内对元素的搬移,基于右值引用(Rvalue reference)的move
建构子都能优化前述动作。在适当的情况下,标准库元件将可利用C++11的语言特性进行升级。这些语言特性包含但不局限以下所列:
move
支援Decltype
此外,自C++标准化之后已经过许多年。现有许多代码利用到了标准库;这同时揭露了部份的标准库可以做些改良。其中之一是标准库的记忆体配置器(allocator)。C++11将会加入一个基于作用域模型的记忆体配置器来支援现有的模型。
虽然C++11会在语言的定义上提供一个记忆体模型以支援执行绪,但执行绪的使用主要将以C++11标准函式库的方式呈现。
C++11标准函式库会提供类别thread
(std::thread
)。若要执行一个执行绪,可以建立一个类别thread
的实体,其初始参数为一个函式物件,以及该函式物件所需要的参数。透过成员函式std::thread::join()
对执行绪会合的支援,一个执行绪可以暂停直到其它执行绪执行完毕。若有底层平台支援,成员函式std::thread::native_handle()
将可提供对原生执行绪物件执行平台特定的操作。
对于执行绪间的同步,标准函式库将会提供适当的互斥锁(像是std::mutex
,std::recursive_mutex
等等)和条件变数(std::condition_variable
和std::condition_variable_any
)。前述同步机制将会以RAII锁(std::lock_guard
和std::unique_lock
)和锁相关演算法的方式呈现,以方便程式员使用。
对于要求高效能,或是极底层的工作,有时或甚至是必须的,我们希望执行绪间的通讯能避免互斥锁使用上的开销。以原子操作来存取记忆体可以达成此目的。针对不同情况,我们可以透过显性的记忆体屏障改变该存取记忆体动作的可见性。
对于执行绪间非同步的传输,C++11标准函式库加入了以及std::packaged_task
用来包装一个会传回非同步结果的函式呼叫。因为缺少结合数个future的功能,和无法判定一组promise集合中的某一个promise是否完成,futures此一提案因此而受到了批评。
更高级的执行绪支援,如执行绪池,已经决定留待在未来的Technical Report加入此类支援。更高级的执行绪支援不会是C++11的一部份,但设想是其最终实现将建立在目前已有的执行绪支援之上。
std::async
提供了一个简便方法以用来执行执行绪,并将执行绪绑定在std::future
。使用者可以选择一个工作是要多个执行绪上非同步的执行,或是在一个执行绪上执行并等待其所需要的资料。预设的情况,实作可以根据底层硬体选择前面两个选项的其中之一。另外在较简单的使用情形下,实作也可以利用执行绪池提供支援。
多元组是一个内由数个异质物件以特定顺序排列而成的资料结构。多元组可被视为是struct
其资料成员的一般化。
由TR1演进而来的C++11多元组型别将受益于C++11某些特色像是可变参数模板。TR1版本的多元组型别对所能容纳的物件个数会因实作而有所限制,且实作上需要用到大量的巨集技巧。相反的,C++11版本的多元组型基本上于对其能容纳的物件个数没有限制。然而,编译器对于模板实体化的递回深度上的限制仍旧影响了元组型别所能容纳的物件个数(这是无法避免的情况);C++11版本的多元组型不会把这个值让使用者知道。
使用可变参数模板,多元组型别的宣告可以长得像下面这样:
template <class ...Types> class tuple;
底下是一个多元组型别的定义和使用情况:
typedef std::tuple <int, double, long &, const char *> test_tuple;
long lengthy = 12;
test_tuple proof (18, 6.5, lengthy, "Ciao!");
lengthy = std::get<0>(proof); // 將proof的第一個元素賦值給lengthy(索引從零開始起跳)
std::get<3>(proof) = " Beautiful!"; // 修改proof的第四個元素
我们可以定义一个多元组型别物件proof
而不指定其内容,前提是proof
里的元素其型别定义了预设建构子(default constructor)。此外,以一个多元组型别物件赋值给另一个多元组型别物件是可能的,但只有在以下情况:若这两个多元组型别相同,则其内含的每一个元素其型别都要定义拷贝建构子(copy constructor);否则的话,赋值操作符右边的多元组其内含元素的型别必须能转换成左边的多元组其对应的元素型别,又或者赋值操作符左边的多元组其内含元素的型别必须定义适当的建构子。
std::tuple< int , double, string > t1;
std::tuple< char, short , const char * > t2 ('X', 2, "Hola!");
t1 = t2 ; // 可行。前兩個元素會作型別轉換,
// 第三個字串元素可由'const char *'所建構。
多元组类型物件的比较运算是可行的(当它们拥有同样数量的元素)。此外,C++11提供两个表达式用来检查多元组类型的一些特性(仅在编译期做此检查)。
std::tuple_size<T>::value
回传多元组T
内的元素个数,std::tuple_element<I, T>::type
回传多元组T
内的第I
个元素的型别在过去,不断有要求想将杂凑表(无序关联式容器)引进标准库。只因为时间上的限制,杂凑表才没有被标准库所采纳。虽然,杂凑表在最糟情况下(如果出现许多冲突(collision)的话)在效能上比不过平衡树。但实际运用上,杂凑表的表现则较佳。
因为标准委员会还看不到有任何机会能将开放定址法标准化,所以目前冲突仅能透过链地址法(linear chaining)的方式处理。为避免与第三方函式库发展的杂凑表发生名称上的冲突,字首将采用unordered而非hash。
函式库将引进四种杂凑表,其中差别在于底下两个特性:是否接受具相同键值的项目(Equivalent keys),以及是否会将键值映射到相对应的资料(Associated values)。
杂凑表类型 | 有无关联值 | 接受相同键值 |
---|---|---|
std::unordered_set |
否 | 否 |
std::unordered_multiset |
否 | 是 |
std::unordered_map |
是 | 否 |
std::unordered_multimap |
是 | 是 |
上述的类别将满足对一个容器类别的要求,同时也提供存取其中元素的成员函式:insert
,erase
,begin
,end
。
杂凑表不需要对现有核心语言做扩展(虽然杂凑表的实作会利用到C++11新的语言特性),只会对标头档<functional>
做些许扩展,并引入<unordered_set>
和<unordered_map>
两个标头档。对于其它现有的类别不会有任何修改。同时,杂凑表也不会依赖其它标准库的扩展功能。
过去许多或多或少标准化的程式库被建立用来处理正规表示式。有鉴于这些演算法的使用非常普遍,因此标准程式库将会包含他们,并使用各种物件导向语言的潜力。
这个新的程式库,被定义于<regex>
标头档,由几个新的类别所组成:
basic_regex
的实体表示match_results
的实体表示函式regex_search
是用来搜寻样式;若要搜寻并取代,则要使用函式regex_replace
,该函式会回传一个新的字串。演算法regex_search
和regex_replace
接受一个正规表示式(样式)和一个字串,并将该样式匹配的情况储存在struct match_results
。
底下描述了match_results
的使用情况:
const char *reg_esp = "[ ,.\\t\\n;:]" ; // 分隔字元列表
std::regex rgx(reg_esp) ; // 'regex'是樣板類'basic_regex'以型別為'char'
// 的參數具現化的實體
std::cmatch match ; // 'cmatch'是樣板類match_results'以型別為'const char *'
// '的參數具現化的實體
const char *target = "Polytechnic University of Turin " ;
// 辨別所有被分隔字元所分隔的字
if( regex_search( target, match, rgx ) )
{
// 若此種字存在
const size_t n = match.size();
for( size_t a = 0 ; a < n ; a++ )
{
string str( match[a].first, match[a].second ) ;
cout << str << "\n" ;
}
}
注意双反斜线的使用,因为C++将反斜线作为跳脱字元使用。但C++11的raw string可以用来避免此一问题。函式库<regex>不需要改动到现有的标头档,同时也不需要对现有的语言作扩展。
这些指针是由TR1智能指标演变而来。注意! 智能指针是类别而非一般指标。
shared_ptr
是一引用计数(reference-counted)指针,其行为与一般C++指标极为相似。在TR1的实作中,缺少了一些一般指针所拥有的特色,像是别名或是指标运算。C++11新增前述特色。
一个shared_ptr
只有在已经没有任何其它shared_ptr
指向其原本所指向物件时,才会销毁该物件。
一个weak_ptr
指向的是一个被shared_ptr
所指向的物件。该weak_ptr
可以用来决定该物件是否已被销毁。weak_ptr
不能被解参考;想要存取其内部所保存的指针,只能透过shared_ptr
。有两种方法可达成此目的。第一,类别shared_ptr
有一个以weak_ptr
为参数的建构子。第二,类别weak_ptr
有一个名为lock
的成员函式,其返回值为一个shared_ptr
。weak_ptr
并不拥有它所指向的物件,因此不影响该物件的销毁与否。
底下是一个shared_ptr
的使用范例:
int main( )
{
std::shared_ptr<double> p_first(new double) ;
{
std::shared_ptr<double> p_copy = p_first ;
*p_copy = 21.2;
} // 此時'p_copy'會被銷毀,但動態分配的double不會被銷毀。
return 0; // 此時'p_first'會被銷毀,動態分配的double也會被銷毀(因為不再有指針指向它)。
}
auto_ptr
将会被C++标准所废弃,取而代之的是unique_ptr
。unique_ptr
提供auto_ptr
大部份特性,唯一的例外是auto_ptr
的不安全、隐性的左值搬移。不像auto_ptr
,unique_ptr
可以存放在C++11提出的那些能察觉搬移动作的容器之中。
C标准库允许使用rand
函数来生成伪随机数。不过其演算法则取决于各程式库开发者。C++直接从C继承了这部份,但是C++11将会提供产生伪乱数的新方法。
C++11的随机数功能分为两部分:第一,一个乱数生成引擎,其中包含该生成引擎的状态,用来产生乱数。第二,一个分布,这可以用来决定产生乱数的范围,也可以决定以何种分布方式产生乱数。乱数生成物件即是由乱数生成引擎和分布所构成。
不同于C标准库的rand
;针对产生乱数的机制,C++11将会提供三种演算法,每一种演算法都有其强项和弱项:
样板类 | 整数/浮点数 | 品质 | 速度 | 状态数* |
---|---|---|---|---|
linear_congruential | 整数 | 低 | 中等[来源请求] | 1 |
subtract_with_carry | 两者皆可 | 中等 | 快 | 25 |
mersenne_twister | 整数 | 佳 | 快 | 624 |
C++11将会提供一些标准分布:uniform_int_distribution(离散型均匀分布),bernoulli_distribution(伯努利分布),geometric_distribution(几何分布),poisson_distribution(卜瓦松分布),binomial_distribution(二项分布),uniform_real_distribution(离散型均匀分布),exponential_distribution(指数分布),normal_distribution(常态分布)和gamma_distribution(伽玛分布)。
底下描述一个乱数生成物件如何由乱数生成引擎和分布构成:
std::uniform_int_distribution<int> distribution(0, 99); // 以離散型均勻分佈方式產生int亂數,範圍落在0到99之間
std::mt19937 engine; // 建立亂數生成引擎
auto generator = std::bind(distribution, engine); // 利用bind將亂數生成引擎和分布組合成一個亂數生成物件
int random = generator(); // 產生亂數
我们可以透过实体化样板类reference_wrapper
得到一个包装引用(wrapper reference)。包装引用类似于一般的引用。对于任意物件,我们可以透过模板类ref
得到一个包装引用(至于constant reference则可透过cref
得到)。
当样板函式需要形参的引用而非其拷贝,这时包装引用就能派上用场:
// 此函數將得到形參'r'的引用並對r加一
void f (int &r) { r++; }
// 樣板函式
template<class F, class P> void g (F f, P t) { f(t); }
int main()
{
int i = 0 ;
g (f, i) ; // 實體化'g<void (int &r), int>'
// 'i'不會被修改
std::cout << i << std::endl; // 輸出0
g (f, std::ref(i)); // 實體化'g<void(int &r),reference_wrapper<int>>'
// 'i'會被修改
std::cout << i << std::endl; // 輸出1
}
这项功能将加入标头档<functional>
之中,而非透过扩展语言来得到这项功能。
针对函数对象的多态包装器(又称多态函数对象包装器)在语义和语法上和函式指标相似,但不像函式指标那么狭隘。只要能被呼叫,且其参数能与包装器相容的都能以多态函数对象包装器称之(函式指标,成员函式指标或仿函式)。
透过以下例子,我们可以了解多态函数对象包装器的特性:
std::function<int (int, int)> func; // 利用樣板類'function'
// 建立包裝器
std::plus<int> add; // 'plus'被宣告為'template<class T> T plus( T, T ) ;'
// 因此'add'的型別是'int add( int x, int y )'
func = &add; // 可行。'add'的型參和回返值型別與'func'相符
int a = func (1, 2); // 注意:若包裝器'func'沒有參考到任何函式
// 會丟出'std::bad_function_call'例外
std::function<bool (short, short)> func2 ;
if(!func2) { // 因為尚未賦值與'func2'任何函式,此條件式為真
bool adjacent(long x, long y);
func2 = &adjacent ; // 可行。'adjacent'的型參和回返值型別可透過型別轉換進而與'func2'相符
struct Test {
bool operator()(short x, short y);
};
Test car;
func = std::ref(car); // 樣板類'std::ref'回傳一個struct 'car'
// 其成員函式'operator()'的包裝
}
func = func2; // 可行。'func2'的型參和回返值型別可透過型別轉換進而與'func'相符
模板类function
将定义在标头档<functional>
,而不须更动到语言本身。
对于那些能自行创建或修改本身或其它程式的程式,我们称之为元编程。这种行为可以发生在编译或执行期。C++标准委员会已经决定引进一组由模板实现的函式库,程式员可利用此一函式库于编译期进行元编程。
底下是一个以元编程来计算指数的例子:
template<int B, int N>
struct Pow {
// recursive call and recombination.
enum{ value = B*Pow<B, N-1>::value };
};
template< int B >
struct Pow<B, 0> {
// ''N == 0'' condition of termination.
enum{ value = 1 };
};
int quartic_of_three = Pow<3, 4>::value;
许多演算法能作用在不同的资料型别;C++模板支援泛型,这使得代码能更紧凑和有用。然而,演算法经常会需要目前作用的资料型别的资讯。这种资讯可以透过型别属性(type traits
)于模板实体化时将该资讯萃取出来。
型别属性能识别一个物件的种类和有关一个型别(class或struct)的特征。标头档<type_traits>
描述了我们能识别那些特征。
底下的例子说明了模板函式‘elaborate’是如何根据给定的资料型别,从而实体化某一特定的演算法(algorithm.do_it
)。
// 演算法一
template< bool B > struct Algorithm {
template<class T1, class T2> static int do_it (T1 &, T2 &) { /*...*/ }
};
// 演算法二
template<> struct Algorithm<true> {
template<class T1, class T2> static int do_it (T1, T2) { /*...*/ }
};
// 根據給定的型別,實體化之後的'elaborate'會選擇演算法一或二
template<class T1, class T2>
int elaborate (T1 A, T2 B)
{
// 若T1為int且T2為float,選用演算法二
// 其它情況選用演算法一
return Algorithm<std::is_integral<T1>::value && std::is_floating_point<T2>::value>::do_it( A, B ) ;
}
此种编程技巧能写出优美、简洁的代码;然而除错是此种编程技巧的弱处:编译期的错误讯息让人不知所云,执行期的除错更是困难。
要在编译期决定一个样板仿函式的回返值型别并不容易,特别是当回返值依赖于函式的参数时。举例来说:
struct Clear {
int operator()(int); // 參數與回返值的型別相同
double operator()(double); // 參數與回返值的型別相同
};
template <class Obj>
class Calculus {
public:
template<class Arg> Arg operator()(Arg& a) const
{
return member(a);
}
private:
Obj member;
};
实体化样板类Calculus<Clear>
,Calculus
的仿函式其回返值总是和Clear
的仿函式其回返值具有相同的型别。然而,若给定类别Confused
:
struct Confused {
double operator()(int); // 參數與回返值的型別不相同
int operator()(double); // 參數與回返值的型別不相同
};
企图实体化样板类Calculus<Confused>将导致Calculus的仿函式其回返值和类别Confused的仿函式其回返值有不同的型别。对于int
和double
之间的转换,编译器将给出警告。
模板std::result_of
被TR1引进且被C++11所采纳,可允许我们决定和使用一个仿函式其回返值的型别。底下,CalculusVer2
物件使用std::result_of
物件来推导其仿函式的回返值型别:
template< class Obj >
class CalculusVer2 {
public:
template<class Arg>
typename std::result_of<Obj(Arg)>::type operator()(Arg& a) const
{
return member(a);
}
private:
Obj member;
};
如此一来,在实体化CalculusVer2<Confused>
其仿函式时,不会有型别转换,警告或是错误发生。
模板std::result_of
在TR1和C++11有一点不同。TR1的版本允许实作在特殊情况下,可以无法决定一个函式呼叫其回返值型别。然而,因为C++11支持了decltype,实作被要求在所有情况下,皆能计算出回返值型别。
iota 函数可将给定区间的值设定为从某值开始的连续值,例如将连续十个整数设定为从 1 开始的连续整数(即 1、2、3、4、5、6、7、8、9、10)。
#include <iostream>
#include <array>
#include <numeric>
std::array<int, 10> ai;
std::iota(ai.begin(), ai.end(), 1);
for(int i: ai){
std::cout<<i<<" ";//1 2 3 4 5 6 7 8 9 10
}
预计由Technical Report提供支援:
延后讨论:
C++编译器对C++11新特性的支持情况:
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.