阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

C++ 20 的悲叹,未出世就被群嘲“劝退”

  • 2019-01-02
  • 本文字数:5667 字

    阅读完需:约 19 分钟

C++ 20的悲叹,未出世就被群嘲“劝退”

为了 C++20,C++标准委员会曾举办历史上规模最大的一次会议(180 人参会),试图通过会议确定哪些特性可以加入新版本,我们也已经看到媒体爆料的部分新特性,比如 Concepts、Ranges、Modules、Coroutines 等,但大部分开发人员并不认可此次调整,并将部分新特性归结为“语法糖”。



不少网友看到上述特性纷纷在社交平台吐槽,表示不看好 C++20 版本的发布:


图片描述


不仅国内如此,国外的一位游戏领域开发人员接连在社交平台发表看法,声明自己不看好 C++20 的新特性,并认为新版本没有解决最关键的问题,他通过使用毕达哥拉斯三元数组示例对 C++20 标准下的代码和旧版本进行对比,明确阐述自己对于 C++20 的态度。


毕达哥拉斯三元数组,C ++ 20 Ranges 风格


以下是 C++20 标准下代码的完整示例:


// A sample standard C++20 program that prints// the first N Pythagorean triples.#include <iostream>#include <optional>#include <ranges>   // New header! using namespace std; // maybe_view defines a view over zero or one// objects.template<Semiregular T>struct maybe_view : view_interface<maybe_view<T>> {  maybe_view() = default;  maybe_view(T t) : data_(std::move(t)) {  }  T const *begin() const noexcept {    return data_ ? &*data_ : nullptr;  }  T const *end() const noexcept {    return data_ ? &*data_ + 1 : nullptr;  }private:  optional<T> data_{};}; // "for_each" creates a new view by applying a// transformation to each element in an input// range, and flattening the resulting range of// ranges.// (This uses one syntax for constrained lambdas// in C++20.)inline constexpr auto for_each =  []<Range R,     Iterator I = iterator_t<R>,     IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun)        requires Range<indirect_result_t<Fun, I>> {      return std::forward<R>(r)        | view::transform(std::move(fun))        | view::join;  }; // "yield_if" takes a bool and a value and// returns a view of zero or one elements.inline constexpr auto yield_if =  []<Semiregular T>(bool b, T x) {    return b ? maybe_view{std::move(x)}             : maybe_view<T>{};  }; int main() {  // Define an infinite range of all the  // Pythagorean triples:  using view::iota;  auto triples =    for_each(iota(1), [](int z) {      return for_each(iota(1, z+1), [](int x) {        return for_each(iota(x, z+1), [](int y) {          return yield_if(x*x + y*y == z*z,            make_tuple(x, y, z));        });      });    });    // Display the first 10 triples    for(auto triple : triples | view::take(10)) {      cout << '('           << get<0>(triple) << ','           << get<1>(triple) << ','           << get<2>(triple) << ')' << '\n';  }}
复制代码


以下代码为简单的 C 函数打印第一个 N Pythagorean Triples:


void printNTriples(int n){    int i = 0;    for (int z = 1; ; ++z)        for (int x = 1; x <= z; ++x)            for (int y = x; y <= z; ++y)                if (x*x + y*y == z*z) {                    printf("%d, %d, %d\n", x, y, z);                    if (++i == n)                        return;                }}
复制代码


如果不必修改或重用此代码,那么一切都没问题。 但是,如果不想打印而是将三元数组绘制成三角形或者想在其中一个数字达到 100 时立即停止整个算法,应该怎么办呢?


毕达哥拉斯三元数组,简单的 C ++风格


以下是旧版本的 C++代码实现打印前 100 个三元数组的完整程序:


// simplest.cpp#include <time.h>#include <stdio.h>int main(){    clock_t t0 = clock();
int i = 0; for (int z = 1; ; ++z) for (int x = 1; x <= z; ++x) for (int y = x; y <= z; ++y) if (x*x + y*y == z*z) { printf("(%i,%i,%i)\n", x, y, z); if (++i == 100) goto done; } done:
clock_t t1 = clock(); printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC); return 0;}
复制代码


我们可以编译这段代码:clang simplest.cpp -o outsimplest,需要花费 0.064 秒,产生 8480 字节可执行文件,在 2 毫秒内运行并打印数字(使用的电脑是 2018 MacBookPro,Core i9 2.9GHz,Xcode 10 clang):


(3,4,5)(6,8,10)(5,12,13)(9,12,15)(8,15,17)(12,16,20)(7,24,25)(15,20,25)(10,24,26)...(65,156,169)(119,120,169)(26,168,170)
复制代码


这是 Debug 版本的构建,优化的 Release 版本构建:clang simplest.cpp -o outsimplest -O2,编译花费 0.071 秒,生成相同大小(8480b)的可执行文件,并在 0ms 内运行(在 clock()的计时器精度下)。


接下来,对上述代码进行改进,加入代码调用并返回下一个三元数组,代码如下:


// simple-reusable.cpp#include <time.h>#include <stdio.h>struct pytriples{    pytriples() : x(1), y(1), z(1) {}    void next()    {        do        {            if (y <= z)                ++y;            else            {                if (x <= z)                    ++x;                else                {                    x = 1;                    ++z;                }                y = x;            }        } while (x*x + y*y != z*z);    }    int x, y, z;};int main(){    clock_t t0 = clock();
pytriples py; for (int c = 0; c < 100; ++c) { py.next(); printf("(%i,%i,%i)\n", py.x, py.y, py.z); }
clock_t t1 = clock(); printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC); return 0;}

复制代码


这几乎在同一时间编译和运行完成,Debug 版本文件变大 168 字节,Release 版本文件大小相同。此示例编写了 pytriples 结构,每次调用 next()都会跳到下一个有效三元组,调用者可随意做任何事情,此处只调用一百次,每次打印三联。


虽然实现的功能等同于三重嵌套 for 循环,但 C++ 20 标准下的代码让人感觉不是很清楚,无法立即读懂程序逻辑。如果 C ++有类似 coroutine 的概念,就可能实现三元组生成器,并且和原始的 for 循环嵌套一样清晰:


generator<std::tuple<int,int,int>> pytriples(){    for (int z = 1; ; ++z)        for (int x = 1; x <= z; ++x)            for (int y = x; y <= z; ++y)                if (x*x + y*y == z*z)                    co_yield std::make_tuple(x, y, z);}
复制代码


C ++20 Ranges 会让整段代码更加清晰吗?结果如下:


auto triples =    for_each(iota(1), [](int z) {        return for_each(iota(1, z+1), [](int x) {            return for_each(iota(x, z+1), [](int y) {                return yield_if(x*x + y*y == z*z,                    make_tuple(x, y, z));                });            });        });
复制代码


多次 return 实在是让人感觉很奇怪,这或许不应该成为好语法的标准。

C++存在的问题有哪些?

如果谈到 C++的问题,至少有两个:一是编译时间;二是运行时性能。虽然 C++ 20 Ranges 还未正式发布,但本文使用了它的近似版,即 isrange-v3(由 Eric Niebler 编写),并编译了规范的“Pythagorean Triples with C ++ Ranges”示例:


// ranges.cpp#include <time.h>#include <stdio.h>#include <range/v3/all.hpp>using namespace ranges;int main(){    clock_t t0 = clock();
auto triples = view::for_each(view::ints(1), [](int z) { return view::for_each(view::ints(1, z + 1), [](int x) { return view::for_each(view::ints(x, z + 1), [](int y) { return yield_if(x * x + y * y == z * z, std::make_tuple(x, y, z)); }); }); });
RANGES_FOR(auto triple, triples | view::take(100)) { printf("(%i,%i,%i)\n", std::get<0>(triple), std::get<1>(triple), std::get<2>(triple)); }
clock_t t1 = clock(); printf("%ims\n", (int)(t1-t0)*1000/CLOCKS_PER_SEC); return 0;}
复制代码


该代码使用 0.4.0 之后的版本,并用 clang ranges.cpp -I. -std=c++17 -lc++ -o outranges 编译,整个过程花费 2.92 秒,可执行文件为 219 千字节,运行在 300 毫秒之内。


这是一个非优化的构建,优化构建版本(clang ranges.cpp -I. -std=c++17 -lc++ -o outranges -O2)在 3.02 秒内编译,可执行文件为 13976 字节,并在 1ms 内运行。因此运行时性能很好,可执行文件稍大,编译时问题仍然存在。


C++20 比简单版本的代码编译时间长近 3 秒


编译时是 C ++的一个大问题,这个非常小的例子编译时间比简单版的 C ++长 2.85 秒。在 3 秒内,现代 CPU 可以进行大量操作,比如,在 Debug 构建中编译一个包含 22 万行代码的数据库引擎(SQLite)只需要 0.9 秒。所以,编译一个简单的 5 行示例代码比运行完整的数据库引擎慢三倍?


在开发过程中,C ++编译时间一直是大小代码库的痛苦根源。他认为,C ++新版本应该把解决编译时问题排在第一位。但是,整个 C++社区好像并不知道该问题,每个版本都将更多内容放入头文件,甚至放入必须存在于头文件的模板化代码中。


range-v3 是 1.8 兆字节的源代码,全部在头文件中,因此,虽然使用 C++ 20 输出 100 个三元数组的代码示例只有 30 行,但加上头文件后,编译器最终会编译 102,000 行代码。在所有预处理之后,简单版本的 C ++示例只有 720 行代码。


调试构建性能差


Ranges 示例的运行时性能慢了 150 倍,这对于要解决实际问题的代码库而言,两个数量级的速度可能意味着不会对任何实际数据集起作用。该开发者在游戏行业工作,这意味着引擎或工具的 Debug 版本不适用于任何真实的游戏级别模拟(性能无法接近所需的交互级别)。


通过避免 STL 位(提交),可以让最终运行时快 10 倍,也可以让编译时间更快且调试更容易,因为微软的 STL 实现特别喜欢深度嵌套的函数调用。这并不是说 STL 必然不好,有可能编写 STL 实现在非优化版本中不会变慢 10 倍(如 EASTL 或 libc ++那样),但由于微软的 STL 过度依赖深度嵌套,因此会变慢。


作为语言的使用者,大部分人不关心它是否正确发展!即便知道 STL 在 Debug 中太慢,宁愿花时间修复或者研究替代方案(例如不使用 STL,重新实现需要的位,或者完全停止使用 C ++)也不会花时间整理论文上报 C++委员会,这太浪费时间。

其他语言如何?

这里简要介绍 C#中“毕达哥拉斯三元数组”实现,以下是完整 C#源代码:


using System;using System.Diagnostics;using System.Linq;class Program{    public static void Main()    {        var timer = Stopwatch.StartNew();        var triples =            from z in Enumerable.Range(1, int.MaxValue)            from x in Enumerable.Range(1, z)            from y in Enumerable.Range(x, z)            where x*x+y*y==z*z            select (x:x, y:y, z:z);        foreach (var t in triples.Take(100))        {            Console.WriteLine($"({t.x},{t.y},{t.z})");        }        timer.Stop();        Console.WriteLine($"{timer.ElapsedMilliseconds}ms");    }}
复制代码


就个人而言,C#可读性较高:


var triples =    from z in Enumerable.Range(1, int.MaxValue)    from x in Enumerable.Range(1, z)    from y in Enumerable.Range(x, z)    where x*x+y*y==z*z    select (x:x, y:y, z:z);
复制代码


用 C ++:


auto triples = view::for_each(view::ints(1), [](int z) {    return view::for_each(view::ints(1, z + 1), [](int x) {        return view::for_each(view::ints(x, z + 1), [](int y) {            return yield_if(x * x + y * y == z * z,                std::make_tuple(x, y, z));        });    });});
复制代码


C#LINQ 的另一种“数据库较少”的形式:


var triples = Enumerable.Range(1, int.MaxValue)    .SelectMany(z => Enumerable.Range(1, z), (z, x) => new {z, x})    .SelectMany(t => Enumerable.Range(t.x, t.z), (t, y) => new {t, y})    .Where(t => t.t.x * t.t.x + t.y * t.y == t.t.z * t.t.z)    .Select(t => (x: t.t.x, y: t.y, z: t.t.z));
复制代码


在 Mac 上编译这段代码,需要使用 Mono 编译器(本身是用 C#编写的),版本 5.16。mcs Linq.cs 需要 0.20 秒。相比之下,编译等效的简单 C#版本需要 0.17 秒。LINQ 样式为编译器创建了额外的 0.03 秒。但是,C ++却创造了额外的 3 秒。


一般来说,我们会试图避免大部分 STL,使用自己的容器,哈希表使用开放寻址代替…甚至不需要标准库的大部分功能。但是,难免需要时间说服每一位新员工(尤其是应届生),因为 C++20 被称为现代 C ++,很多新员工认为“新一定就是好”,其实并不是这样。

为什么 C ++会这样?

该开发者表示不太清楚 C++为什么会发展到现在这个地步。 但他个人认为,C++社区需要学会“保持接近 100%向后兼容的同时发展一种语言”。 在某种程度上,现在的 C++生态系统太专注于炫耀或证明其价值的复杂性,却并不易用。


在他的印象中,大多数游戏开发人员还停留在 C++ 11、14 或者 17 版本,C++20 基本忽略了一个问题,无论什么被添加到标准库,编译时间长和调试构建性能差的问题没有解决都是无用的。


对于游戏产业而言,大部分传统技术是用 C 或 C ++构建的,在很长一段时间内,没有出现可行的替代品(好在目前至少可以用 Rust 作为可能的竞争者),对 C 和 C ++的依赖程度很高,需要社区的一些帮助和回应。


参考链接:http://aras-p.info/blog/2018/12/28/Modern-C-Lamentations/


2019-01-02 08:2019403
用户头像
赵钰莹 InfoQ 主编

发布了 874 篇内容, 共 604.1 次阅读, 收获喜欢 2671 次。

关注

评论 9 条评论

发布
用户头像
更正一下:STL is too slow in Debug 指的是编译器debug模式
这个确实是,因为模板代码跟普通代码不一样,模板代码编译器“决定权”占比要大的多,最经典的例子就是inline, stl里很少直接标注inline, 因为inline不inline决定权几乎完全在编译器,普通代码则有一定的决定权。

2021-11-09 20:36
回复
用户头像
最后说一下:
这老哥还说了一些关于c的什么什么,懒得反驳。 2021年在看c++20, 虽然没与11那样具有里程碑的意义,还是可圈可点的,既兼顾了higher-level了,也有lower。同时提出了一种不同与OOP的编程思想,如果真的实现,可能会颠覆整个软件世界。
ps:这网站能不能早点专业的人翻译啊?别翻译些啊猫啊狗的博客啊? Devid, Douglas,
Anthony, Hans 这些人的博客不香吗?
2021-11-09 20:23
回复
用户头像
4:最后图穷匕现。第一个是“the companies have already wrote their own containers/strings/algorithms/… years ago”(意思就是stl没有用,如果需要我们自己会写,你们committees浪费时间,等等), 第二个是“C++ is nuts and is not what we want”
反驳:stl可以说是几乎所有c++ programmer的启蒙老师,它提供了一个generic范式,提供一种普适的解决方案,不可能适合所有应用场景,去要求do everything with stl is enough,可能吗?写了20年的c++,我敢肯定也是师从stl,回过头来骂老师,行!真行!,第二个跟不值一驳了,你们搞游戏引擎的要求就是我们的要求?就应该是committees的要求?太自大了吧?老哥
2021-11-09 20:11
回复
用户头像
3 : 接着就是开始集中喷STL,看了下。两个方面,第一个方面是“compilation Times Are a Big Issue”,第二个方面就是“STL is too slow in Debug”。
反驳:generic是种什么思维啊?是高抽象思维,高抽象必然意味着低效率(这个效率指的是编译效率,而非运行效率),不会吧不会吧写了20年的c++,这还要我说的啊?,generic debug当然难啊,如果知道实例化过程完全有迹可循,吃了generic的红利还不想付出点代价,哪里有这么好的事?
2021-11-09 19:42
回复
用户头像
2: 用simple C++ style 对比 generic style, 得出一个结论:“Maybe that ....right now.”(意思就是说:你看simple style可读性多高,还拿perl反讽一下,同时还自我嘲讽一下);
反驳:我都不想反驳了,送他一句话:pls, get fk back to c.
2021-11-09 19:29
回复
用户头像
粗略的看了下原文章,这网站的翻译是个二把刀,没明白这作者在说啥。作者主要说了一下几点:
1:他说的这个“一是编译时间;二是运行时性能”,主要是指STL,原文“Issues with Everything is a library” 。
反驳:这完全是一种c的思维,往作者背景看了一眼,怪不得。。。搞游戏引擎开发的。。。我作为搞分布式的,我当然有意见。。分布式要求高抽象,高效率。游戏引擎唯一也是最高要求就是效率,一切为效率让步。(这货喷c++跟llinus一个观点)
2021-11-09 19:21
回复
用户头像
真搞笑,C++的例子generic and constexpr, 与c#的例子抽象程度都不一样,c++ constexpr编译期行为当然编译时间久(其目的就是把“已知留编译期,把未知留在运行期”),搞笑的是敢让C#骑在c++头上举例,我还是第一次见!!!!最搞笑的是“业内人事举例什么什么”,哪个业内人士啊,这种代码风格?,把名字列出来好吗?半桶水就隔着指点江山,不害臊!
2021-11-09 18:31
回复
用户头像
真搞笑,C++的例子generic and constexpr, 与c#的例子抽象程度都不一样,c++ constexpr编译期行为当然编译时间久(其目的就是把“已知留编译期,把未知留在运行期”),搞笑的是敢让C#骑在c++头上举例,我还是第一次见!!!!最搞笑的是“业内人事举例什么什么”,哪个业内人士啊,这种代码风格?,把名字列出来好吗?半桶水就隔着指点江山,不害臊!
ps:无意之间看见的这篇文章,特意注册来反驳一下,简直误人子弟,怪不得国内c++不行。
2021-11-09 18:30
回复
特地注册过来点赞评论!国内C++环境太差了,媒体也是不负责任,大家都喜欢偷懒图快图省事!
2023-10-07 09:59 · 广东
回复
没有更多了
发现更多内容

极客大学 - 架构师训练营 第九周

9527

将减少阻力的香蕉法则,运用在软件开发上会产生什么效果?

Philips

敏捷开发 快速开发 企业应用

架构师训练营 2 期 Week04 总结

架构训练营 - 第8周课后作业 - 学习总结

Pudding

第8周作业

paul

架构师训练营第一期 - 第八周课后作业

卖猪肉的大叔

极客大学架构师训练营

【架构师训练营 1 期】第八周作业

诺乐

第四周总结

jizhi7

架构师训练营第一期 - 第八周学习总结

卖猪肉的大叔

极客大学架构师训练营

找出两个链表中合并的元素

虽然世界给我们变化,但让我们的人生更向幸福靠近一点点,而入门票就是自学这回事

叶小鍵

浅谈软件研发管理体系建设

大黄蜂

架构师训练营第 1 期第 8 周作业

owl

极客大学架构师训练营

架构师 01 期,第八周课后作业

子文

第四周作业

jizhi7

极客大学架构师训练营

【架构师训练营 1 期】第八周学习总结

诺乐

SpringBoot中的响应式web应用

程序那些事

spring WebFlux 程序那些事 响应式系统 spring 5

面试重灾区——Synchronized深度解析

执墨

并发编程 synchronized 内存布局 CAS 锁升级

不可思议,竟然还有人不会查看GC垃圾回收日志?

田维常

垃圾回收 GC

详解快速开发平台与工作流通用组件的设计规范

Philips

敏捷开发 快速开发 企业应用

性能优化(文件、数据结构、算法、网络IO)

ABS

week4-一个典型的大型互联网应用系统使用了哪些技术方案和手段,主要解决什么问题?请列举描述。

未来已来

week4-作业二:根据当周学习情况,完成一篇学习总结

未来已来

互联网应用架构目标及技术方案

第八周作业

springboot+java+redis 简单实用的搜索栏热搜,个人历史记录,文字过滤

灰尘子

Week4 系统架构

贺志鹏

极客大学架构师训练营

架构师训练营 - 第 8 周课后作业(1 期)

Pudding

架构师训练营第八周课程笔记及心得

Airs

第八周总结

《Java程序员修炼之道》.pdf

田维常

C++ 20的悲叹,未出世就被群嘲“劝退”_编程语言_赵钰莹_InfoQ精选文章