what to benefit from the C++14 Standard
Posted xiongping_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了what to benefit from the C++14 Standard相关的知识,希望对你有一定的参考价值。
转载:http://weibo.com/ttarticle/p/show?id=2309404029139492430854
我同时也是哥伦比亚的客座教授,尽管我辞掉了Texas A&M的教职,但在校方坚持下,我现在还算是一名教授。
好的,接下来我会简单介绍:
- 我们希望从编程语言中获得什么,编程语言在开发环境中扮演者什么角色
- 一个使用C++11的简单例子
- 然后我会过一遍C++11/C++14中的特性
- 最后进入问答环节
这次演讲不可能涵盖C++11/14 中所有的东西,这不是半个小时的演讲能做到的。如果你有问题的话,可以翻阅一些资料,或许可以得到解答。
当我们讨论编程语言时,人们往往被编程语言的特性吸引,好像这是唯一重要的东西。我们并不这样认为,我们从不认为语言特性是最有意思的部分。
我们关心的是如何构建系统。一个较好的系统应该具备这些要素:吞吐量大、延迟小、bug少、可维护性高、开发速度快,等等。这些才是我们真正想要的。编程语言应该尽力去实现这些目标。如果我们太过关注于怎么写一个循环,或者指针应该怎样表示,或者诸如此类的问题,我们就会忽视那些更重要的东西。显然,不能这么做。
上面这些才是我们应该尽力去做到的。编程语言只是“工具链”的一环,如果我们手里有两种编程语言,它们在以上方面都很好,但某个更优的“工具链”,那还是能分出优劣来。如果对于正在搭建的应用或者系统,没有使用正确的“工具链”的话,问题也会随之而来,所以我们必须注意这些问题。
另外,我们要改善语言,但不是为了改变而改变。我们应该清楚作出改善改变的原因,以及需要达到什么样效果。当我在思考语言,想它的用处的时候,我总是在思考:“Bug 是从哪儿来的?Bug 隐藏在什么地方?我如何才能避免犯错?又该如何去发现已经犯下的错误?”
而这很大程度上和简化代码有关。作为一门成熟的编程语言,我们现在的一大问题是:很多人在90年代按照当时的风格编写了C++代码。这不算错误,但是,现如今仍然有人按照90年代的方式写代码,这让我觉得很懊恼,因为我们明明可以把代码写得更好。我们应该思考如何才能做的更好,以达到最初的目标,并且思考如何摆脱旧代码,消除其中存在的问题。
下面我将会举一个例子,也是唯一一个例子,展示如何用C++11 来简化代码。
三周前,我从一个朋友那拿了一段代码,这是现实中的代码。这段代码取自一个发布了8年之久的产品中。这段代码写得很好,写代码的人也是熟手。这段代码没有什么根本性错误,我得强调一下。
它大概是这样的:他们想要从头遍历一个容器(额,那个声明应该放这里,抱歉...),寻找一个特定的模式,如果符合该模式,进入if条件句执行代码,否则转入其他语句。
通过C++11,我们可以简化这部分操作。我们仅仅定义一个迭代器,指向容器开头,每次迭代地址加1,直到容器末尾,寻找符合条件的模式。
这段代码中有两点需要注意:
代码变短了,因为不需要写出迭代器的类型。不清楚你们是否喜欢编写这么冗长的代码,但我肯定不会。这样写太麻烦了,应该避免!
此外,由于我们将迭代器变量的定义放到for中,所以cit的生命周期被局限在for循环里,它也本该如此。在那段实际代码中,我们必须非常注意变量cit不能在for循环体外面被访问。如果万一真的被用到,我们必须知道为什么,因为cit已指向容器末尾。这是一个简单的循环,它正确而简短,不错。
他们看了一会儿代码后意识到:在C++11中,我们不仅仅有auto,我们有range-for语句,专为编写这种最简单的循环,即对于 v 中的每一个 x,尝试匹配某个模式,然后做一些事情。他们如此简化了代码。所以在C++11中你可以用range-for语法来描述一个这种简单循环。
对于简化后的代码,我们知道在 for 循环体中是不可以修改循环变量的,这里也没有可以修改的循环变量。但在之前的代码中,要看出这只是一个普通的循环遍历确实有点困难。我不是故意要制造Bug,但却由于失误造成了Bug。这Bug不在我朋友发给我的代码中,但不管怎样,看这里。
“从头到尾遍历一个容器”的意思在这段代码中显然不是那么直观,但在这段被简化代码中,非常直观。我们还有其他方式来实现,“遍历一个容器中的每一个元素,查看有没有我们想要的”这一想法吗?
当然,我们可以用标准库里的 find_if() 函数,所以,我们最终把这段代码化解为 find_if() 函数。比起我们一开始的代码,这个解决方案更容易,也更简洁。为何8年前他们在第一次写这个代码的时候没有用最简单的方法(find_if())呢?他们是出色的程序员,即便如此,也没有发现化解的办法。
那是因为代码量太大,阻碍了视线。然而,经历了这一系列的化简步骤,结果就会变得很明显:当初他们应该直接采取最简洁的代码来实现(利用find_if())。
有意思的是,最优方法其实是C++98的解决方案。尽管通过C++11去发现这个问题也花费了一些过程,或许他们也会发现find_if()这一方法,但事实证明没有。这是一个真实的案例。
还有一些我非常不喜欢的地方。我希望在find_if()时只关注 v 容器本身,而不是从v.begin()看到v.end()。这个版本的代码可读性更好,直接表达了我想要的。
为了实现它,我们需要自己定义一个新的find_if() 函数来再一次简化代码。
这里还剩下一点可以在C++11里进行优化:如何表达容器Cont的iterator 呢?
答案是使用类型/模板别名,所以typename T::iterator可以用Iterator来表示。注意代码以后不需要到处写typename。通过类型函数的帮助,我可以得到如此简洁优雅的代码,我想不出更简单的了。
稍稍考虑了一会后,我回复了我朋友,问他:“要是5年前,我跟你说我能把你的产品代码量减半,你会是什么反应?”
我曾经过了一遍代码,当然咯,不是文件中所有代码都能化简,也不是所有的代码都能缩成一行,所以代码的总长度不会像示例中的这样变成原来的五分之一。在生产环境的代码中,我们或者他们将代码量减了一半。
要是五年前我告诉你这个你会怎么说?回复给的是:大力丸。就是说,这种一看就是销售人员的忽悠套路。不能信的。但不管怎么说,他自行做了重构。而且很明显,这很管用。如今这段代码正在生产环境下工作,部署之后的事情我就没听他提过了。照他的说法,这段代码和重构前一样地精巧、快速。简化并没有带来什么额外的损失。
我从中学到了一件事,就是渐进式的改进确实是很重要的。你不必一开始就洞悉细节。这办不到,大部分开发人员都办不到。只要能不断前进一点点,不断做出小的改进,最终这些会积少成多,将代码的规模减半,而且没有额外的性能开销。方向很重要,这样就有了进展,这就是进展。
我一直跟你们说,我们应该让简单的事情简单化。泛化当然很好,可以做任何事,处理任何情况。但有时候也会适得其反,你面对着模版文件、模版参数……或者是带一堆参数、处理繁冗细节的函数。我的意思是,你当然应该做到那些,但是你也应该能用简单的手法完成简单的任务。
那么,如何定义简单?简单意味着表达……代码直接表达出意图。看下这里的代码,很直接地描述了功能。
我们在做find_if操作,在容器 v 中查找匹配的模式,直接而精确地描述了我们在做什么。
之前的代码则很复杂,暴露了不必要的细节,增加了维护的复杂度。简单的代码理解起来就更容易,这就是简单化的含义。如果你了解标准库的algorithm,那你应该知道find_if()指的是什么,准确描述了你在做的事。我觉得这种写法很漂亮,而且有意思是,如果你将事物简单地、清晰地、明确地描述出来,那你就能理解它了。然后优化器、编译器都能理解它了。
另外,最简单的代码实际上常常是最快的。当然,不是所有的代码都能变得简单。有时候我们要做一些混乱的事,有时候我们面对的系统接口不是清晰定义的,有时候我们要和乱七八糟的硬件打交道,有时候我们要处理协议、隐藏的设计以及类似的东西,不要让这些接口的复杂度窜入你的程序中。试着用简单的接口隐藏它们,试着找出你真正想要做的事,让那件事成为你的接口,将复杂性隐藏在其后。
OK~下面我打算聊点关于标准和标准化进程的东西。
也许你们有人还记得,1998年我们就有一个标准,它撑了很长一段时间。直到2011年我们有了一个新的标准。事情是这样的,我们本来一直打算在2007年推行新标准。当时大家觉得,像Fortran、C、C++以及Ada这些ISO标准化的语言,差不多十年会发布一个新版本。
这就造成,如果我们在2007年强推新标准,时间太赶,有人就不得不把一些东西推迟到2017年的标准中,这让人很失落。所以他们往里面加了大把大把大把他们的东西,这样到了2008年。他们觉着有些东西现在不加进去,就得捱到2018年了。于是他们就又向标准里加了更多的内容。这样我们就到了2009年。实际上,我们到了2011年才结束这个过程。
我们当中有些人,尤其是Herb Sutter(Herb Sutter在微软工作,也是标准化组织WG21的会议召集人),觉得这样算不上太好,我们不应该赶着点。所以我们决定使用火车模型。火车在2014年离开站台,要是你错过了,下一趟会在2017年发车。要是你又没赶上,2021年还有一趟。
我们讨论过应该隔多久发一趟车,最终决定是大约三年的样子。我强调这个,是因为我们发现一个现象,大伙儿总是尝试在发车前最后一分钟登车,导致列车延误。所以我们决定在2011年做一次大规模发布,2014年做一次小规模,然后2017年时又会有一次大规模发布。
有意思的是,我们准时发布了,两周前新标准通过了,我们有了C++14,也就是目前的新标准。现在正好是2014年,我们做到了,通过了要发布的标准。
编译器实现是什么情况呢?C++98从发布到有很好的编译器实现,花了差不多5年的时间。C++11时有了进步,去年我们有一些编译器实现了C++11,并且所有的编译器表现都很不错。今年我们有了两个编译器完整支持C++14,还有一个编译器也相当不错。也就是说,我们不仅仅准时发布标准。我们还随之准时发布了符合标准的编译器实现。你们要知道,之前没人做到过这点,这确实很了不起。
为了实现这点,我们采用了并行的方法。我们建立了很多研究组,这些组做文档和技术规范的工作。这样先放出一些ISO文档(如Nxxxx.pdf)。编译器作者依据这些文档实现语言特性。然后我们可以调整技术细节,融入我们在实际运用中发现的修改,最终整合进下一版标准。到达这里(2017年标准)确实有些交通拥堵。不过我们会想些办法的。
前面提过,C++14是一次小规模发布,其实算是完成了C++11。当你要发布一个大型项目时,总有些功能无法一起发布,因为总有特征蔓延。有些事情,只有产品开始使用后才会意识到。这就是C++14之于C++11的意义。它保含一些我们在C++11发布之后才会学到的东西。要是你们对下一次主发布的C++17有哪些内容感兴趣,这些是研究组及各自联系人的名字列,数目还不少。
要想多了解一些的话,你们可以前往C++官网:isocpp.org。在那里你点击“标准”,然后可以往下搜寻更多类似的信息。要是想追踪最新动态,你首先就该访问isocpp.org,因为上面有很多文章值得一读。要是官网满足不了你,那你可以去找这些文档和工作组。
只要你真正感兴趣,联系了这当中一些人后,你就可以加入进来,贡献自己的力量。来看下,这里有一系列的新特性。
特性列表真的很枯燥。我希望能给你们看更多的示例,可惜没有多少时间。所以我打算给你们看下这个列表,大致介绍下每一条。我会大致说下哪里能找到更多的信息,然后在问答环节,你们可以问我具体的问题。而不是花上3天时间解释每个条目。
C++11标准中一个很重要的事情,是为这门现代编程语言定义一个好的并发编程模型。如果之前看过C/C++标准,你会发现并发这种东西基本上不存在,全都是单线程计算,关于这一点,我们已经搞定了。我们很好地处理了并发性,而且不会降低任何程序的运行速度。
在C++11中,我们曾希望尽早了结相关问题,但是事情并不顺利。
我们最初的内存模型和并发模型是从Java借鉴来的,Java的并发模型不错,似乎也能用,而Java的这个模型确实也曾在我们考虑范围之内,但是不得不放弃。如果C++采纳了Java的内存模型,将导致Java的虚拟机的运行速度变慢(因为JVM是C++写的),所以在C++中不得不做一些更为复杂,更为高效的事情。
“切记切记不要使用Java的内存模型”, 这是Java专家这样告诉我们的。所以我们没有采用,这耽误了C++11的进度。最终我们有了一个确实很棒的内存模型,它一点也不影响性能,你可以放心。我们可以一如既往地使用线程和互斥器。这种方式并不是最好的并发编程手法,甚至可能是最差的一种,但是在系统级别上编程你必须知道这种写法。
我们还有了 atomics 和 auto,以及无锁编程。所以如果你觉得线程与互斥器的代价太大,你可以使用标准的方式来实现自己的同步原语,这个实现是可移植的,并且是类型安全的,这一点非常棒。再也没有 void ** , 再也不用为Windows和Linux编写不同的版本。我们已经能做得更好,这是一个重要的点。
接下来还有一些库,这里有计时支持,如果你还在用clock,那你就太落伍了。现在你可以更轻松而准确地测量时间。说到随机数,不要再用原来的rand(),我们有了更好的随机数,原来的rand()函数对你的程序不利。正则表达式,你现在可以在C++中使用正则表达式了。这里还有一些类型萃取,还有(模板)元编程支持,他们出现在这并不是因为我们想要鼓励元编程,他们在这是因为我们想简化元编程,使它不至于失控。
这里还有些好东西:unique_ptr, shared_ptr, make_shared(), make_unique()...可以让你更准确、简单、安全地管理资源。
哈希表终于被加入进来了!哈希表是我们在C++98时漏掉的一个特性,因为时间来不及了。我们花了半年多时间来确定哈希表的细节,结果就是,大家都构造了他们自己的哈希表。较广泛被使用的哈希表就有六种,我们一定要做出一个高质量的哈希表。因为市面上已经有六种哈希表,不能输给它们。他们之中的一些已经进入了std namespace,这是不应该发生的。所以我们做了unordered_map, unordered_set, 等等(多重集),以上就是哈希表。
我们还加入了元组。标准中还有其他一些较为普遍的提升:容器、算法。这边这个是move语义,我们可以移动一些东西,我们可以给函数按值传入一个vector,还可以让函数按值返回一个。受益于移动语义,这些操作很廉价。我可以从函数中按值返回一个包含上亿元素的vector,直接return它就行。代价大概是3次指针赋值,这也很棒。
C++14 TS中还有对文件系统的支持,使得我们可以控制文件、路径,还有目录什么的。我们还加了一个叫optional的类型,有些人非常喜欢这个,这等于是一个可选的值,你可以判断其值是否存在。
这里的语言特性确实很多,因为这个版本是一个主版本发布,不过基本上这使得C++看起来像是一个新语言了,你得学习一些新的东西,才能享受其益处。
如果你还是比较喜欢用原来的方式来写程序,没问题,但你一样会遇到你原来的bug,获得原来的性能表现,还有原来的维护上的问题。如果你用新的方式写代码,你能感觉到升级 , 当你写下这些东西的时候,它的感觉是完全不一样的。你们当中如果有尝试过的人,会明白我的意思。还没有试过的,请相信我。如果你们信不过我,去问问那些试过的人就知道了。
两者之间有些特别大的差异,我这里列出了move语义,auto还有初始化列表。现在有了初始化列表,所以你不必再现建立一个空的容器让后用其他东西填充,或者用默认值初始化完再改变其中的值。你只需要给出你的初始化列表就好了,可变参数模板,因此类模板可以有任意多个类型参数,函数可以有任意多个形参。
这里还有很多...那个是别名,我已经给出用法了。这里还有像是数字的分组分离,如果你想写一个超级长的数字字面量。你可以在这个变量中正确的地方插入单引号来将他们分隔开。如果你想说一千万的话,你不需要输入,还要担心自己到底有没有输入对正确的位数,你可以就写10'000'000 这样的形式。
我们说了上面一些小东西,还有再之前move语义和initializer这样真正大的东西。这里我就跳过一些东西,我们并不打算完全地把它们过一遍,我前面已经说过了。这些个东西都已经发布了, 你完全可以使用它们。
对于GCC, clang, MS(C++)这些编译器,我都或多或少在使用一些,并且我大部分的代码在这三个编译器上都运行良好。额~如果你还没有玩过C++14的话,去找个可以用的编译器试试看如何。
这些新特性中有很多都可以用来提高你的代码性能。我们所提供的语言的简洁性不以牺牲代码性能为代价,那不是我们想要的。C++应该成为最快的一种语言,并且它确实是这样的,并且在越来越快。比如说,你在C++11中会用到constexpr关键字,并且在C++14中会更多地用到。
你可以写出一个在编译时就能完成求值的表达式,这比运行时计算要来的快得多,因为它根本不需要在运行时做计算,你可以编译时就写出类型安全的函数和代码。move语意意味着着你不再需要指针打过多交道,这也有助于提升你的程序性能。
事实上,如果你使用C++14编译器重新编译C++98代码的话,往往发现速度变快了,因为move语义发挥了作用。即使你还没有对代码做任何改动,你已经在使用着最新的标准库。很多新东西都被设计出来,以简化我们编写的最常见的代码。比如前面一开始我给你们展示的那个小循环的例子。
是的,这些很琐碎,但你知道,它正变得甚至更琐碎这样才能更容易书写。以解放我们的脑力,用于那些真正值得我们花费脑力的东西上。简化维护很重要,所以我们在传统的复杂写法之外有了新的选择。
我们应尽量避免裸指针、裸的new和delete。我只要是在代码中看到一句delete,我基本能肯定代码在某处会有一个Bug。因为如果你需要delete,你很可能就会忘记了delete某样东西,你也可能两次释放了同一个对象,这就更糟糕了。如果delete没有隐藏在析构函数,或者专门设计来处理资源管理的函数里面,那么这段代码很可能有Bug。如果我读一段代码,我会看看有没有delete,如果有,那我很有信心改进这代码。
相似的,如果我看到一个new,那要么别处会有delete,这就回到了前面的情况,要么忘了delete。所以我并不想看到代码中出现,除非new出现在构造函数里,或着出现在资源handler的构造函数实参里。那才是它应该处的位置。如果new在其他地方出现,就往往属于裸new,那很可能会产生Bug。这也意味着,指针的出现同样值得怀疑,因为new和delete一定会和指针打交道。
这些内容我和很多人讲过并举过例子。而且每次都会有人说到这些语言特性会被误用。当然有人真的会误用。你见到过不会被误用的语义特性么?无论它是新的还是旧的。我的意思是我真的听说过各种说法。
早年,有人说C语言的for语句会被误用,因为它能做的事情太多了,也确实有人会误用它。虚函数也是常常被误用的。虚函数很有用么?是的。他们会被误用么?亦然。因此我们需要好好学习如何使用它。模板,当然可以被误用了。今天,很多C++程序取得高性能的关键之处就在于使用了模板。这个一个螺丝刀,非常古老的工具,它可以被错误地使用么?嗯。(点头表示同意)除了打开铁罐头之外,它还被拿来做了什么呢?所以,对这种批评不要放在心上。
值得花心思的是学会什么才是合适的用法,至少要考虑好什么是对这些新特性的负责任的用法。不要恐慌,也不要只是为了使用新特性而做过头的事情。你要搞懂哪些是应该避免的,哪些可以实际使用。所以,请认小心地学习。所有的特性曾经都是新的。他们是新特性,但不久之后就会成为旧的,我们会发明出更好的东西出来。
这是我从Herb Sutter的演讲中摘录的一个例子。这是一种我们大部分人在C++ 98标准及以前写的代码。它做了什么呢?先创建了一个circle对象,再从文件加载了一堆shape(形状),放入元素为shape指针的vector容器,然后做了一些事情。
在这个例子中,我就做了这件事:用一个循环去检查是否有shape和这个circle匹配,真的很简单。这只是个基本的代码例子,你肯定也见过,这段代码可以正常工作。代码还行,在1990年代算得上是好代码。在某个时候我们还必须记得做一些清理工作。因为这里我们从文件中加载了一些shape对象,所以我们必须去delete这些对象。好的,尽管这曾经是好代码,但现在不是了。
而今天我们看到的代码是这个样子的,创建一个circle,保存在unique_ptr中,具有对象的所有权。这意味着一旦unique_ptr离开作用域,对象就被delete。谓之“智能指针”也。将会在析构的时候delete其指向的对象。
我们加载了这些shape对象,保存到auto变量中,(指上一行)这里我说了要一个unique_ptr,所以类型就不必重复写出。load_shapes()这一行也不必重复写出类型,因为编译器已经知道其类型了。
接下来我对其中的元素做了一个循环,这里我用了range-for循环,否则代码和前面一样。好处就体现出来了, 这一段代码中,我不需要再用指针了。因为circle归unique_ptr管,而load_shape()会返回vector<unique_ptr>或vector<shared_ptr>。因此,资源管理的问题就不存在了。这就是全部的代码,没有出现delete。此外,旧的代码不是异常安全的,而新的代码是对异常安全的,尽管我都没有明确处理异常。这好多了。
以上就是我演讲的所有内容,下面是问答环节。建忠,交给你了。(问答环节省略)
以上是关于what to benefit from the C++14 Standard的主要内容,如果未能解决你的问题,请参考以下文章
What to do when Enterprise Manager is not able to connect to the database instance (ORA-28001)
What is the best way to memorize or remember what you study/read?
What's the difference between using “let” and “var” to declare a variable in JavaScript?
What exactly is the parameter e (event) and why pass it to JavaScript functions?
What is the best way to handle Invalid CSRF token found in the request when session times out in Spr
powershell 来自https://stackoverflow.com/questions/5466329/whats-the-best-way-to-determine-the-locatio