为了C++20,C++标准委员会曾举办历史上规模最大的一次会议(180人参会),试图通过会议确定哪些特性可以加入新版本,我们也已经看到媒体爆料的部分新特性,比如Concepts、Ranges、Modules、Coroutines等,但大部分开发人员并不认可此次调整,并将部分新特性归结为“语法糖”。
\n不少网友看到上述特性纷纷在社交平台吐槽,表示不看好C++20版本的发布:
\n不仅国内如此,国外的一位游戏领域开发人员接连在社交平台发表看法,声明自己不看好C++20的新特性,并认为新版本没有解决最关键的问题,他通过使用毕达哥拉斯三元数组示例对C++20标准下的代码和旧版本进行对比,明确阐述自己对于C++20的态度。
\n毕达哥拉斯三元数组,C ++ 20 Ranges风格
\n以下是C++20标准下代码的完整示例:
\n// A sample standard C++20 program that prints\n// the first N Pythagorean triples.\n#include \u0026lt;iostream\u0026gt;\n#include \u0026lt;optional\u0026gt;\n#include \u0026lt;ranges\u0026gt; // New header!\n \nusing namespace std;\n \n// maybe_view defines a view over zero or one\n// objects.\ntemplate\u0026lt;Semiregular T\u0026gt;\nstruct maybe_view : view_interface\u0026lt;maybe_view\u0026lt;T\u0026gt;\u0026gt; {\n maybe_view() = default;\n maybe_view(T t) : data_(std::move(t)) {\n }\n T const *begin() const noexcept {\n return data_ ? \u0026amp;*data_ : nullptr;\n }\n T const *end() const noexcept {\n return data_ ? \u0026amp;*data_ + 1 : nullptr;\n }\nprivate:\n optional\u0026lt;T\u0026gt; data_{};\n};\n \n// \u0026quot;for_each\u0026quot; creates a new view by applying a\n// transformation to each element in an input\n// range, and flattening the resulting range of\n// ranges.\n// (This uses one syntax for constrained lambdas\n// in C++20.)\ninline constexpr auto for_each =\n []\u0026lt;Range R,\n Iterator I = iterator_t\u0026lt;R\u0026gt;,\n IndirectUnaryInvocable\u0026lt;I\u0026gt; Fun\u0026gt;(R\u0026amp;\u0026amp; r, Fun fun)\n requires Range\u0026lt;indirect_result_t\u0026lt;Fun, I\u0026gt;\u0026gt; {\n return std::forward\u0026lt;R\u0026gt;(r)\n | view::transform(std::move(fun))\n | view::join;\n };\n \n// \u0026quot;yield_if\u0026quot; takes a bool and a value and\n// returns a view of zero or one elements.\ninline constexpr auto yield_if =\n []\u0026lt;Semiregular T\u0026gt;(bool b, T x) {\n return b ? maybe_view{std::move(x)}\n : maybe_view\u0026lt;T\u0026gt;{};\n };\n \nint main() {\n // Define an infinite range of all the\n // Pythagorean triples:\n using view::iota;\n auto triples =\n for_each(iota(1), {\n return for_each(iota(1, z+1), = {\n return for_each(iota(x, z+1), = {\n return yield_if(x*x + y*y == z*z,\n make_tuple(x, y, z));\n });\n });\n });\n // Display the first 10 triples\n for(auto triple : triples | view::take(10)) {\n cout \u0026lt;\u0026lt; '('\n \u0026lt;\u0026lt; get\u0026lt;0\u0026gt;(triple) \u0026lt;\u0026lt; ','\n \u0026lt;\u0026lt; get\u0026lt;1\u0026gt;(triple) \u0026lt;\u0026lt; ','\n \u0026lt;\u0026lt; get\u0026lt;2\u0026gt;(triple) \u0026lt;\u0026lt; ')' \u0026lt;\u0026lt; '\\n';\n }\n}\n
\n
以下代码为简单的C函数打印第一个N Pythagorean Triples:
\nvoid printNTriples(int n)\n{\n int i = 0;\n for (int z = 1; ; ++z)\n for (int x = 1; x \u0026lt;= z; ++x)\n for (int y = x; y \u0026lt;= z; ++y)\n if (x*x + y*y == z*z) {\n printf(\u0026quot;%d, %d, %d\\n\u0026quot;, x, y, z);\n if (++i == n)\n return;\n }\n}\n
\n
如果不必修改或重用此代码,那么一切都没问题。 但是,如果不想打印而是将三元数组绘制成三角形或者想在其中一个数字达到100时立即停止整个算法,应该怎么办呢?
\n毕达哥拉斯三元数组,简单的C ++风格
\n以下是旧版本的C++代码实现打印前100个三元数组的完整程序:
\n// simplest.cpp#include \u0026lt;time.h\u0026gt;#include \u0026lt;stdio.h\u0026gt;\nint main(){\n clock_t t0 = clock();\n\n int i = 0;\n for (int z = 1; ; ++z)\n for (int x = 1; x \u0026lt;= z; ++x)\n for (int y = x; y \u0026lt;= z; ++y)\n if (x*x + y*y == z*z) {\n printf(\u0026quot;(%i,%i,%i)\\n\u0026quot;, x, y, z);\n if (++i == 100)\n goto done;\n }\n done:\n\n clock_t t1 = clock();\n printf(\u0026quot;%ims\\n\u0026quot;, (int)(t1-t0)*1000/CLOCKS_PER_SEC);\n return 0;\n}\n
\n
我们可以编译这段代码:clang simplest.cpp -o outsimplest,需要花费0.064秒,产生8480字节可执行文件,在2毫秒内运行并打印数字(使用的电脑是2018 MacBookPro,Core i9 2.9GHz,Xcode 10 clang):
\n(3,4,5)\n(6,8,10)\n(5,12,13)\n(9,12,15)\n(8,15,17)\n(12,16,20)\n(7,24,25)\n(15,20,25)\n(10,24,26)\n...\n(65,156,169)\n(119,120,169)\n(26,168,170)\n
\n
这是Debug版本的构建,优化的Release版本构建:clang simplest.cpp -o outsimplest -O2,编译花费0.071秒,生成相同大小(8480b)的可执行文件,并在0ms内运行(在clock()的计时器精度下)。
\n接下来,对上述代码进行改进,加入代码调用并返回下一个三元数组,代码如下:
\n// simple-reusable.cpp#include \u0026lt;time.h\u0026gt;#include \u0026lt;stdio.h\u0026gt;\nstruct pytriples\n{\n pytriples() : x(1), y(1), z(1) {}\n void next()\n {\n do\n {\n if (y \u0026lt;= z)\n ++y;\n else\n {\n if (x \u0026lt;= z)\n ++x;\n else\n {\n x = 1;\n ++z;\n }\n y = x;\n }\n } while (x*x + y*y != z*z);\n }\n int x, y, z;\n};\nint main(){\n clock_t t0 = clock();\n\n pytriples py;\n for (int c = 0; c \u0026lt; 100; ++c)\n {\n py.next();\n printf(\u0026quot;(%i,%i,%i)\\n\u0026quot;, py.x, py.y, py.z);\n }\n\n clock_t t1 = clock();\n printf(\u0026quot;%ims\\n\u0026quot;, (int)(t1-t0)*1000/CLOCKS_PER_SEC);\n return 0;\n}\n\n
\n
这几乎在同一时间编译和运行完成,Debug版本文件变大168字节,Release版本文件大小相同。此示例编写了pytriples结构,每次调用next()都会跳到下一个有效三元组,调用者可随意做任何事情,此处只调用一百次,每次打印三联。
\n虽然实现的功能等同于三重嵌套for循环,但C++ 20标准下的代码让人感觉不是很清楚,无法立即读懂程序逻辑。如果C ++有类似coroutine的概念,就可能实现三元组生成器,并且和原始的for循环嵌套一样清晰:
\ngenerator\u0026lt;std::tuple\u0026lt;int,int,int\u0026gt;\u0026gt; pytriples()\n{\n for (int z = 1; ; ++z)\n for (int x = 1; x \u0026lt;= z; ++x)\n for (int y = x; y \u0026lt;= z; ++y)\n if (x*x + y*y == z*z)\n co_yield std::make_tuple(x, y, z);\n}\n
\n
C ++20 Ranges会让整段代码更加清晰吗?结果如下:
\nauto triples =\n for_each(iota(1), {\n return for_each(iota(1, z+1), = {\n return for_each(iota(x, z+1), = {\n return yield_if(x*x + y*y == z*z,\n make_tuple(x, y, z));\n });\n });\n });\n
\n
多次return实在是让人感觉很奇怪,这或许不应该成为好语法的标准。
\nC++存在的问题有哪些?
\n如果谈到C++的问题,至少有两个:一是编译时间;二是运行时性能。虽然C++ 20 Ranges还未正式发布,但本文使用了它的近似版,即isrange-v3(由Eric Niebler编写),并编译了规范的“Pythagorean Triples with C ++ Ranges”示例:
\n// ranges.cpp#include \u0026lt;time.h\u0026gt;#include \u0026lt;stdio.h\u0026gt;#include \u0026lt;range/v3/all.hpp\u0026gt;\nusing namespace ranges;\nint main(){\n clock_t t0 = clock();\n\n auto triples = view::for_each(view::ints(1), {\n return view::for_each(view::ints(1, z + 1), = {\n return view::for_each(view::ints(x, z + 1), = {\n return yield_if(x * x + y * y == z * z,\n std::make_tuple(x, y, z));\n });\n });\n });\n\n RANGES_FOR(auto triple, triples | view::take(100))\n {\n printf(\u0026quot;(%i,%i,%i)\\n\u0026quot;, std::get\u0026lt;0\u0026gt;(triple), std::get\u0026lt;1\u0026gt;(triple), std::get\u0026lt;2\u0026gt;(triple));\n }\n\n clock_t t1 = clock();\n printf(\u0026quot;%ims\\n\u0026quot;, (int)(t1-t0)*1000/CLOCKS_PER_SEC);\n return 0;\n}\n
\n
该代码使用0.4.0之后的版本,并用clang ranges.cpp -I. -std=c++17 -lc++ -o outranges编译,整个过程花费2.92秒,可执行文件为219千字节,运行在300毫秒之内。
\n这是一个非优化的构建,优化构建版本(clang ranges.cpp -I. -std=c++17 -lc++ -o outranges -O2)在3.02秒内编译,可执行文件为13976字节,并在1ms内运行。因此运行时性能很好,可执行文件稍大,编译时问题仍然存在。
\nC++20比简单版本的代码编译时间长近3秒
\n编译时是C ++的一个大问题,这个非常小的例子编译时间比简单版的C ++长2.85秒。在3秒内,现代CPU可以进行大量操作,比如,在Debug构建中编译一个包含22万行代码的数据库引擎(SQLite)只需要0.9秒。所以,编译一个简单的5行示例代码比运行完整的数据库引擎慢三倍?
\n在开发过程中,C ++编译时间一直是大小代码库的痛苦根源。他认为,C ++新版本应该把解决编译时问题排在第一位。但是,整个C++社区好像并不知道该问题,每个版本都将更多内容放入头文件,甚至放入必须存在于头文件的模板化代码中。
\nrange-v3是1.8兆字节的源代码,全部在头文件中,因此,虽然使用C++ 20输出100个三元数组的代码示例只有30行,但加上头文件后,编译器最终会编译102,000行代码。在所有预处理之后,简单版本的C ++示例只有720行代码。
\n调试构建性能差
\nRanges示例的运行时性能慢了150倍,这对于要解决实际问题的代码库而言,两个数量级的速度可能意味着不会对任何实际数据集起作用。该开发者在游戏行业工作,这意味着引擎或工具的Debug版本不适用于任何真实的游戏级别模拟(性能无法接近所需的交互级别)。
\n通过避免STL位(提交),可以让最终运行时快10倍,也可以让编译时间更快且调试更容易,因为微软的STL实现特别喜欢深度嵌套的函数调用。这并不是说STL必然不好,有可能编写STL实现在非优化版本中不会变慢10倍(如EASTL或libc ++那样),但由于微软的STL过度依赖深度嵌套,因此会变慢。
\n作为语言的使用者,大部分人不关心它是否正确发展!即便知道STL在Debug中太慢,宁愿花时间修复或者研究替代方案(例如不使用STL,重新实现需要的位,或者完全停止使用C ++)也不会花时间整理论文上报C++委员会,这太浪费时间。
\n其他语言如何?
\n这里简要介绍C#中“毕达哥拉斯三元数组”实现,以下是完整C#源代码:
\nusing System;using System.Diagnostics;using System.Linq;\nclass Program\n{\n public static void Main()\n {\n var timer = Stopwatch.StartNew();\n var triples =\n from z in Enumerable.Range(1, int.MaxValue)\n from x in Enumerable.Range(1, z)\n from y in Enumerable.Range(x, z)\n where x*x+y*y==z*z\n select (x:x, y:y, z:z);\n foreach (var t in triples.Take(100))\n {\n Console.WriteLine($\u0026quot;({t.x},{t.y},{t.z})\u0026quot;);\n }\n timer.Stop();\n Console.WriteLine($\u0026quot;{timer.ElapsedMilliseconds}ms\u0026quot;);\n }\n}\n
\n
就个人而言,C#可读性较高:
\nvar triples =\n from z in Enumerable.Range(1, int.MaxValue)\n from x in Enumerable.Range(1, z)\n from y in Enumerable.Range(x, z)\n where x*x+y*y==z*z\n select (x:x, y:y, z:z);\n
\n
用C ++:
\nauto triples = view::for_each(view::ints(1), {\n return view::for_each(view::ints(1, z + 1), = {\n return view::for_each(view::ints(x, z + 1), = {\n return yield_if(x * x + y * y == z * z,\n std::make_tuple(x, y, z));\n });\n });\n});\n
\n
C#LINQ的另一种“数据库较少”的形式:
\nvar triples = Enumerable.Range(1, int.MaxValue)\n .SelectMany(z =\u0026gt; Enumerable.Range(1, z), (z, x) =\u0026gt; new {z, x})\n .SelectMany(t =\u0026gt; Enumerable.Range(t.x, t.z), (t, y) =\u0026gt; new {t, y})\n .Where(t =\u0026gt; t.t.x * t.t.x + t.y * t.y == t.t.z * t.t.z)\n .Select(t =\u0026gt; (x: t.t.x, y: t.y, z: t.t.z));\n
\n
在Mac上编译这段代码,需要使用Mono编译器(本身是用C#编写的),版本5.16。mcs Linq.cs需要0.20秒。相比之下,编译等效的简单C#版本需要0.17秒。LINQ样式为编译器创建了额外的0.03秒。但是,C ++却创造了额外的3秒。
\n一般来说,我们会试图避免大部分STL,使用自己的容器,哈希表使用开放寻址代替…甚至不需要标准库的大部分功能。但是,难免需要时间说服每一位新员工(尤其是应届生),因为C++20被称为现代C ++,很多新员工认为“新一定就是好”,其实并不是这样。
\n为什么C ++会这样?
\n该开发者表示不太清楚C++为什么会发展到现在这个地步。 但他个人认为,C++社区需要学会“保持接近100%向后兼容的同时发展一种语言”。 在某种程度上,现在的C++生态系统太专注于炫耀或证明其价值的复杂性,却并不易用。
\n在他的印象中,大多数游戏开发人员还停留在C++ 11、14或者17版本,C++20基本忽略了一个问题,无论什么被添加到标准库,编译时间长和调试构建性能差的问题没有解决都是无用的。
\n对于游戏产业而言,大部分传统技术是用C或C ++构建的,在很长一段时间内,没有出现可行的替代品(好在目前至少可以用Rust作为可能的竞争者),对C和C ++的依赖程度很高,需要社区的一些帮助和回应。
\n参考链接:http://aras-p.info/blog/2018/12/28/Modern-C-Lamentations/
\n