C++11语言扩展:常规特性

本节内容:auto、decltype、基于范围的for语句、初始化列表、统一初始化语法和语义、右值引用和移动语义、Lambdas、noexcept防止抛出异常、constexpr、nullptr——一个指针空值常量、复制并再抛出异常、内联命名空间、用户自定义数据标识。

auto

推导

在这里因为它的初始化类型我们将得到x的int类型。一般来说,我们可以写

x的类型我们将会根据初始化表达式“ expression”的类型来自动推导。

当一个变量的类型很难准确的知道或者写出的时候,用atuo通过初始化表达式的类型进行推导显然是非常有用的。

参考:

在C++98里我们必须这样写

当一个变量的类型取决于模板实参的时候不用auto真的很难定义,例如:

 

从T*U的表达式,我们人类的思维是很难理清tmp的类型的,但是编译器肯定知道T和U经过了什么特殊处理。

auto的特性在最早提出和应用时是有区别的:1984年初,Stroustrup在他的Cfront实现里用到了它,但是是由于C的兼容问题被迫拿来用的,这些兼容的问题已经在C++98和C++99接受了隐性int时候消失了。也就是说,现在这两种语言要求每一个变量和函数都要有一个明确的类型定义。auto旧的含义(即这是一个局部变量)现在是违法的。标准委员会的成员们在数百万行代码中仅仅只找到几百个用到auto关键字的地方,并且大多数出现在测试代码中,有的甚至就是一个bug。

auto在代码中主要是作为简化工具,并不会影响标准库规范。

参考:

decltype

decltype(E)的类型(“声明类型”)可用名称或表达E来声明。例如:

这个概念已经流行在泛型编程的标签“typeof”很长一段时间,但实际使用的“领域”的实现是不完整和不兼容,所以标准版命名了decltype。

注意:喜欢使用auto时,你只是需要一个变量的类型初始化。如果你需要一个类型不是一个变量,那么你需要用到decltype,例如返回类型。

参考:

  • the C++ draft 7.1.6.2 Simple type specifiers
  • [Str02] Bjarne Stroustrup. Draft proposal for “typeof”. C++ reflector message c++std-ext-5364, October 2002. (original suggestion).
  • [N1478=03-0061] Jaakko Jarvi, Bjarne Stroustrup, Douglas Gregor, and Jeremy Siek: Decltype and auto (original proposal).
  • [N2343=07-0203] Jaakko Jarvi, Bjarne Stroustrup, and Gabriel Dos Reis: Decltype (revision 7): proposed wording.

基于范围的for循环

声明的范围像是STL-sequence定义的begin()和end(),允许你在这个范围内循环迭代。所有标准容器可以作为一个范围来使用,比如可以是std::string,初始化器列表,一个数组,和任何你可以定义begin()和end()的,比如istream。例如:

你可以看到,V中所有的元素都从begin()开始迭代循环到了end()。另一个例子:

begin()(和end())可以被做为x.begin()的成员或一个独立的函数被称为开始(x)。成员版本优先。

参考:

初始化列表

推导

初始化列表不再只针对于数组了。定义一个接受{}初始化列表的函数(通常是初始化函数)接受一个std::initializer_list < T >的参数类型,例如:

初始化器列表可以是任意长度的,但必须同种类型的(所有元素必须的模板参数类型,T,或可转换T)。

一个容器可能实现一个初始化列表构造函数如下:

直接初始化和复制初始化的区别是对初始化列表的维护,但是因为初始化列表的相关联的频率就降低了。例如std::vector有一个int类型显示构造函数和initializer_list构造函数:

函数可以作为一个不可变的序列访问initializer_list。例如:

仅具有一个std::initializer_list的单参数构造函数被称为初始化列表构造函数。

标准库容器,string类型及正则表达式均具有初始化列表构造函数,以及(初始化列表)赋值函数等。一个初始化列表可被用作Range,例如,表达式Range。

初始化列表是一致泛化初始化解决方案的一部分。他们还防止类型收窄。一般来说,你应该通常更喜欢使用{ }来代替()初始化,除非你想用c++98编译器来分享代码或(很少)需要使用()调用没initializer_list重载构造函数。

参考:

统一初始化语法和语义

c++ 98提供了几种方法初始化一个对象根据其类型和初始环境。滥用时,会产生可以令人惊讶的错误和模糊的错误消息。推导:

要记得初始化的规则并选择最好的方法去初始化是比较难的。

C++11的解决方法是允许所有的初始化使用初始化列表

重点是,x{a}在在执行代码中都创建了一个相同的值,所以在使用“{}”进行初始化合法的情况下都产生了相同的结果。例如:

参考:

右值引用和移动语义

左值(用在复制操作符左边)和右值(用在复制操作符右边)的区别可以追溯到Christopher Strachey (C++遥远的祖先语言CPL和外延语义之父)的时代。在C++中,非const引用可以绑定到左值,const引用既可以绑定到左值也可以绑定要右值。但是右值却不可以被非const绑定。这是为了防止人们改变那些被赋予新值之前就被销毁的临时变量。例如:

如果incr(0)被允许,那么就会产生一个无法被人看到的临时变量被执行增加操作,或者更糟的0会变成1.后者听起来很傻,但实际上确实存在这样一个bug在Fortran编译器中:为值为0的内存位置分配。

到目前为止还好,但考虑以下代码:

如果T是一个复制元素要付出昂贵代价的类型,比如string和vector,swap将会变成一个十分昂贵的操作(对于标准库来说,我们有专门化的string和vector来处理)。注意一下这些奇怪的现象:我们并不想任何变量拷贝。我们仅仅是想移动变量a,b和tmp的值。

在C++11中,我们可以定义“移动构造函数”和“移动赋值操作符”来移动,而不是复制他们的参数:

& &表明“右值引用”。一个右值引用可以绑定到一个右值(而不是一个左值):

赋值这个操作的背后思想,并不是拷贝,它只是构造一个源对象的代表然后再替换。例如,string s1 = s2的移动,它不是产生s2的拷贝,而是让s1把s2中字符变为自己的同时删除自己原有的字符串(也可以放在s2中,但是它也面临着被销毁)

我们如何知道是否可以简单的从源对象进行移动?我们可以告诉编译器:

move(x)只是意味着你“你可以把x当作一个右值”,

如果把move()称做eval()也许会更好,但是现在move()已经用了好多年了。在c++11中,move()模板(参考简介)和右值引用都可以使用。

右值引用也可以用来提供完美的转发。

在C++0x的标准库中,所有的容器都提供了移动构造函数和移动赋值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最终结果是,在无用户干预时,标准容器和算法的性能都提升了,因为复制操作的减少。

参考:

lambdas

Lambda表达式是一种描述函数对象的机制,它的主要应用是描述某些具有简单行为的函数(译注:Lambda表达式也可以称为匿名函数,具有复杂行为的函数可以采用命名函数对象,当然,简单和复杂之间的划分依赖于编程人员的选择)。例如:

参数 [&](int a, int b) { return abs(a) < abs(b); }是一个”lambda”(又称为”lambda函数”或者”lambda表达式”), 它描述了这样一个函数操作:接受两个整形参数a和b,然后返回对它们的绝对值进行”<“比较的结果。(译注:为了保持与代码的一致性,此处应当为”[] (int a, int b) { return abs(a) < abs(b); }”,而且在这个lambda表达式内实际上未用到局部变量,所以 [&] 是无必要的)

一个Lambda表达式可以存取在它被调用的作用域内的局部变量。例如:

有人认为这“相当简洁”,也有人认为这是一种可能产生危险且晦涩的代码的方式。我的看法是,两者都正确。

[&] 是一个“捕捉列表(capture list)”,用于描述将要被lambda函数以引用传参方式使用的局部变量。如果我们仅想“捕捉”参数v,则可以写为: [&v]。而如果我们想以传值方式使用参数v,则可以写为:[=v]。如果什么都不捕捉,则为:[]。将所有的变量以引用传递方式使用时采用 [&], [=] 则相应地表示以传值方式使用所有变量(译注:“所有变量”即指lambda表达式在被调用处,所能见到的所有局部变量)。

如果某一函数的行为既不通用也不简单,那么我建议采用命名函数对象或者函数。例如,如上示例可重写为:

对于简单的函数功能,比如记录名称域的比较,采用函数对象就略显冗长,尽管它与lambda表达式生成的代码是一致的。在C++98中,这样的函数对象在被用作模板参数时必须是非本地的(译注:即你不能在函数对象中像此处的lambda表达式那样使用被调用处的局部变量),然而在C++中(译注:意指C++0x),这不再是必须的。

为了描述一个lambda,你必须提供:

  • 它的捕捉列表:它可以使用的变量列表(除了形参之外),如果存在的话(”[&]” 在上面的记录比较例子中意味着“所有的局部变量都将按照引用的方式进行传递”)。如果不需要捕捉任何变量,则使用 []。
  • (可选的)它的所有参数及其类型(例如: (int a, int b) )。
  • 组织成一个块的函数行为(例如:{ return v[a].name < v[b].name; })。
  • (可选的)采用了新的后缀返回类型符号的返回类型。但典型情况下,我们仅从return语句中去推断返回类型,如果没有返回任何值,则推断为void。

参考:

noexcept防止抛出异常

如果一个函数不能抛出异常或者一个程序没有对函数抛出的异常进行处理,那么这个函数可以用关键字noexcept进行修饰,例如:

如果一个被noexcept修饰的函数抛出了异常(所以异常会跳出呗noexcept修饰的函数),程序会调用std::terminate()这个函数来终止程序。在对象被明确定义的状态下不能调用terminate();比如无法保证析构函数正常调用,不能保证栈的自动释放,也无法保证在遇到任何问题时重新启动。故意这样的使noexcept成为一种简单“粗暴”而有效的处理机制-它比旧的处理机制throw()动态抛出异常要有效的多。

它可以让一个函数根据条件来实现noexcept修饰。比如,一个算法可以根据他的模板参数来决定自己是否抛出异常。

这里,第一个noexcept被用作操作符operator:如果if f(v.at(0))不能够抛出异常,noexcept(f(v.at(0)))则返回true,所以f()和at()是无法抛出异常noexcept。

noexcept()操作符是一个常量表达式,并且不计算表达式的值。

声明的通常形式是noexcept(expression),并且单独的一个“noexcept”关键字实际上就是的一个noexcept(true)的简化。一个函数的所有声明都必须与noexcept声明保持 兼容。

一个析构函数不应该抛出异常;通常,如果一个类的所有成员都拥有noexcept修饰的析构函数,那么这个类的析构函数就自动地隐式地noexcept声明,而与函数体内的代码没有关系。

通常,将某个抛出的异常进行移动操作是一个很坏的主意,所以,在任何可能的地方都用noexcept进行声明。如果某个类的所有成员都有使用noexcept声明的析构函数,那么这个类默认生成的复制或者移动操作(类的复制构造函数,移动构造函数等)都是隐式的noexcept声明。(?)

noexcept 被广泛地系统地应用在C++11的标准库中,以此来提供标准库的性能和满足标准库对于简洁性的需求。

参考:

constexpr

常量表达式机制:

  • 提供了更多的通用的值不发生变化的表达式
  • 允许用户自定义的类型成为常量表达式
  • 提供了一种保证在编译期完成初始化的方法

考虑下面这段代码:

在这里,常量表达式关键字constexpr表示这个重载的操作符“|”就应该像一个简单的表单一样,如果它的参数本身就是常量 ,那么这个操作符应该在编译时期就应该计算出它的结果来。
除了可以在编译时期被动地计算表达式的值之外,我们希望能够主动地要求表达式在编译时期计算其结果值,从而用作其它用途,比如对某个变量进行赋值。当我们在变量声明前加上constexpr关键字之后,可以实现这一功能,当然,它也同时会让这个变量成为常量。

通常,我们希望编译时期计算可以保护全局或者名字空间内的对象,对名字空间内的对象,我们希望它保存在只读空间内。
对于那些构造函数比较简单,可以成为常量表达式(也就是可以使用constexpr进行修饰)的对象可以做到这一点(?)

  •  const的主要功能是修饰一个对象而不是通过一个接口(即使对象很容易通过其他接口修改)。只不过声明一个对象常量为编译器提供了优化的机会。特别是,如果一个声明了一个对象常量而他的地址没有取到,编译器通常可以在编译时对他进行初始化(尽管这不是肯定的)保证这个对象在他的列表里而不是把它添加到生成代码里。
  • constexpr的主要功能可以在编译时计算表达式的值进行了范围扩展,这是一种计算安全而且可以用在编译时期(如初始化枚举或者整体模板参数)。constexpr声明对象可以在初始化编译的时候计算出结果来。他们基本上只保存在编译器的列表,如果需要的话会释放到生成的代码里。

参考:

nullptr 一个指针空值常量

nullptr是一个指针空值常量,不是一个整数。

参考:

复制并再抛出异常

你如何捕获一个异常然后把它抛出到另一个线程?使用标准文档18.8.5里描述的标准库的魔力方法吧。

exception_ptr current_exception(); 正在处理的异常(15.3)或者正在处理的异常的副本(拷贝)将返回一个exception_ptr 变量,如果当前没有遇到异常,返回值为一个空的exception_ptr变量。只要exception_ptr指向一个异常,那么至少在exception_ptr的生存期内,运行时能够保证被指向的异常是有效的。

void rethrow_exception(exception_ptr p);
template exception_ptr copy_exception(E e); 它的作用如同:

当我们需要将异常从一个线程传递到另外一个线程时,这个方法十分有用的。

内联命名空间

内联命名空间机制是通过一种支持版本更新的机制来支持库的演化,推导:

我们这里有一个命名空间Mine包含最新版本的(V99)和前一个版本(V98),如果你想要显式应用(某个版本的函数),你可以:

内联的关键是使内联命名空间的声明和直接在外围命名空间声明一样。

lnline是静态的及面向实现的设施,它由命名空间的设计者放置来帮助用户进行选择。对于Mine的用是不可以说“我想要的命名空间是V98而不是V99”。

参照:

  • Standard 7.3.1 Namespace definition [7]-[9].

用户自定义数据标识

C++提供了很多内置的数据标识符(2.14节变量)

built-in types (2.14 Literals):

然而,爱C++98里并没有用户自定义的数据标识符。这就有悖于甚至冲突“用户自定义类型和内置leiixng一样得到支持”的原则。特殊情况下,人们有这样的需求:

C++11支持“用户自定义数据标识”通过在变量名后面加一个后缀来标定所需类型,例如:

注意constexpr的使用可以在编译时期计算。有了这个功能,我们可以这样写:

基本(实现)方法是编译器在解析什么语句代表一个变量之后,再分析一下后缀。用户自定义数据标识机制只是简简单单的允许用户制定一个新的后缀,并决定如何对它之前的数据进行处理。要想重新定义一个内建的数据标识的意义或者它的参数、语法是不可能的。一个数据标识操作符可以使用它(前面)的数据标识传递过来的处理过的值(如果是使用新的没有定义过的后缀的值)或者没有处理过的值(作为一个字符串)。
要得到一个没有处理过的字符串,只要使用一个单独的const char*参数即可,例如:

这个C语言风格的字符串”1234567890123456789012345678901234567890″被传递给了操作符 operator”” x()。注意,我们并没有明确地把数字转换成字符串。

有以下四种数据标识的情况,可以被用户定义后缀来使用用户自定义数据标识:

  • 整型标识:允许传入一个unsigned long long或者const char*参数
  • 浮点型标识:允许传入一个long double或者const char*参数
  • 字符串标识:允许传入一组(const char*,size_t)参数
  • 字符标识:允许传入一个char参数。

注意,你为字符串标识定义的标识操作符不能只带有一个const char*参数(而没有大小)。例如:

根本原因是如果我们想有一个“不同的字符串”,我们同时也想知道字符的个数。后缀可能比较短(例如,s是字符串的后缀,i是虚数的后缀,m是米的后缀,x是扩展类型的后缀),所以不同的用法很容易产生冲突,我们可以使用namespace(命名空间)来避免这些名字冲突:

参考:

4 收藏 2 评论

关于作者:christian

I am an onion and one cries while peeling it (新浪微博:@Onion_christian李阳) 个人主页 · 我的文章

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部