学C++越学越有味是怎么回事

有人说:『语言只是工具,为了实现不同的需求,使用最顺手的工具才是最好的。』

刚学习编程,啥也不会的新手这样想:『我擦,大神说的话,听起来好有哲学,好有道理,赶紧记下来。』

刚精通一门语言,正在学习其他语言的人想:『这句话就是废话。只要我用 XXX 语言实现某个需求 的技术栈,我用 XXX 语言同样能实现这个需求,而且比原来的 YY 语言做得更好,开发效 率更高!』

学习了很多门语言,掌握了很多技术的大牛笑而不语。

在 2003 年,有人在邮件列表上争论过闭包和对象两种抽象方式的优劣,这场讨论创造了那 个著名的一句话:『对象是穷人的闭包,反之亦 然』

编程语言通常是图灵完全的,A 语言可以做某些业务,理论上 B 语言也能做。但编程语言 的设计往往是需求驱动的。语言之间的差异,更多是取舍,针对他要解决的痛点所作出的取 舍。

比如 Golang 本来是 Google 开发来顶替 Python 的语言,很多设计比较粗糙。但是 Go 比 C 方便的同时还保留了 C 的一些快糙猛的风格,最后大火。可惜语言本身缺乏很多基础设 施(比如错误处理和泛型),现在不得已开始擦屁股,Golang 2 就准备加入泛型。

又比如说 Rust,Rust 的设计比较精巧,可以写出很安全的代码。但是如果用 Rust 写语言 虚拟机,那么 Rust 的安全保障就会成为阻碍,VM 里常用的 tagged pointer,threading intepreter,exception handling 技术往往涉及指针算术,goto,汇编,setjmp/longjmp 等不安全代码,用 Rust 只会给自己徒增烦恼。

为什么 Lua 是最流行的嵌入脚本语言?因为 Lua 实在太小了!Lua 不去做多余的事情。因 此可以很好的融合进 C/C++ 代码里面。Lua 的字符串的内部表示就是 char *,不支持 Unicode character (用 string.char 函数没法接受大于 255 的值作为 char)和 C/C++ 的风格完美契合。 Lua 更没有自己的 Object system,用户可以直接整合 C 的对象系统, 不用担心这种针对自己业务的改造会带来语言上的 incosistency。

但是 Lua 几乎没有自己的生态。因为一千个项目里,有一千个 Lua。每个项目用到的 Lua 看起来都是 Lua,实际相去甚远,因为他们调用的都是自己项目里面提供的 C 函数,Lua只 是作为一个胶水语言写逻辑代码。

同样的道理,可以推广开到计算机科学的很多地方。

Q:我能用递归下降手写 parser,还可以用 parser combinator 这种好东西,为什么还要 研究 LL,LR,LALR?

A:LL,LR,LALR 这些文法,表达能力比不上自己手写,但是你能用 LL 和 LR 生成的语法, 意味着语法必然没有歧义。

Q:Prolog 语言能灵活地描述对象之间的关系,为什么我还需要 SQL 这种蹩脚语言来访问 数据库?

A:因为 Prolog 太过强大,容易写出低效的查询语句,用 SQL 足以应对大多数查询用途。

Q:大家都知道 DSL 不好,为什么还是孜孜不倦地发明 DSL?

A:因为 DSL 真的有用,在特定领域真的比 GPL(general-purpose language)高效。

……

11 个赞

软件工程充满了 trade-off . 当过分狂热的吹捧一门语言的时候,就丢失了语言设计本身 trade-off 的细节,是不全面的。

C 与 C++ 的争论,其实可以说是两种设计思路的碰撞

  1. 我撒手不管,只定义了最核心,最基本,但是可以组合的抽象。比如 struct(没有继承, 只是用来存放数据),函数(first class function,函数指针 + userdata 当闭包)。 if,循环(递归)等核心理念,其他全部交给第社区自己搞第三方库。

  2. 我是大管家,我全部都要管,我的标准库特别大。我定义 OO 要用 class 和继承。我 coroutine 要用 stackless。大家都基于我的库开发。

后者理想中的优势是,制定标准的人可以让社区里大部份的人心悦诚服,语言的生态将十分 统一,有且只有一种正确的方式去做一件事情。用户编程起来也更方便,不用担心同一个功 能要纠结使用哪一个库了。如果标准制定者们齐心协力,跟进编程界最新最流行的技术,及 时改进标准,语言也能很快跟进,开发者们可以直接用这个语言开发最新的技术。

我悲观的感觉,第二种思路是必定失败的。因为程序员的脾气就是反权威 Who can Who up No BB,勇于尝试新技术的。微软尝试过很多次一统江湖,IE 垄断过浏览器市场,COM 技术 解决跨语言调用的 ABI issue,SilverLight 解决网页多媒体开发。但是微软都被开放的技 术(Firefox & 其他高级语言如 Java,微软后来学习 Java 先进经验基于 COM 开发了 NET framework & HTML5)给打败了,这些技术过时之后成了拖慢微软前进脚步的绊脚石。

C++ 已经搞得如此复杂,但是仍然没有务实一点,标准库的理念都没有搞好。STL 之外还有 Boost,甚至 Qt 自己也要搞一套标准库出来自己用。

比如说 C++ 的模板,下面的程序是没法编译的,因为 std::cout<<没有对 unique_ptr 重载。

#include <memory>
#include <iostream>

int main() {
  auto p = std::make_unique<int>(3);
  // 正确写法:
  // std::count << *p;
  std::cout << p;
  return 0;
}
citreu@asus-laptop:~$ g++ --version
g++ (GCC) 11.2.0
Copyright © 2021 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。
citreu@asus-laptop:~$ g++ ~/tmp/test.cpp 2>&1 | wc -l
284

用 GCC 编译这个错误的程序,竟然报了 284 行错误。template 编译错误除了在模板使用 处报错,还会在展开的地方都报错。C++ 在上世纪 90 年代提出模板,而 C++ 20 才搞出来 Constraints & Concepts 。在模板实例化前就约束好模板参数,以解决编译器报错难以理解的问题。到了 C++ 20, C++的模板才勉强从拉绳火枪变成一把栓式步枪。为什么不是自动步枪?因为只要 template 实例化时 concept 被满足,那么你甚至可以调用 concept 上没有定义,但是实现上有定义 的方法……参见此 处

C++ concept-constrained templates are still only type checked when concrete instantiation is attempted – they just give better, sooner error messages for types that don’t comply with the constraint, rather than the long stream of nonsense that failed template instantiations output in C++ without concepts.

借用《C 的回归》 里面的一段话。

但是,STL 过于庞大了,Boost 更加是。我不是抱怨阅读和学习它们的源码的难度和需要的 时间和精力。正相反,我在学习它们的过程中充满了乐趣和感激之情。高手前辈透过这些高 质量的代码教会了我很多东西。我隐隐担心的是,这么庞大的代码,它的设计不可能是永远 正确的。两年之后,他们的设计肯定依旧正确,再两年还是的。但是我几乎敢肯定,放之更 长远的时间来看,绝对会在某些设计领域发现其不是最佳的选择。到那一天,我们会选择修 改吗?我想 C++ 社区会被迫选择妥协。但是,C++ 程序员心中会充满痛苦。

C 在这个问题上的抉择是不一样的。在效率问题上,C 程序里最令人担心的是函数调用的消 耗。C++ 程序员最津津乐道的案例就是 std::sort 全面击败了 C 库中的 qsort 。C 语言 的失败正在于多余的函数调用消耗。

但是,从一开始 C 就选择了承认函数调用的消耗,而这一点几乎是唯一。付出了这个代价 后,设计失误导致的效率下降问题几乎总可以避免。C 和 C++ 都可以选择重写设计失败的 部分,但不一样的是, C 程序员几乎可以不考虑妥协的问题。同样的是考虑极端效率的语 言,C 语言坦然面对缺陷,才是真正的符合了 KISS 原则。

正如我最近看的一篇文章,和大家分享一下

震惊!C++ STL 实现太落后,Rust 的 std::slice::sort_unstable 竟然吊打 C++!

10 个赞

C++ 用户说:『我基本上兼容 C,而且我有很多高级特性!你想要底层控制力,那么你可 以在 C++ 里面写 C 啊。』

在说这个之前,我先用 Common Lisp 举个例子。

Emacs 论坛里,大家或多或少都听说过 Common Lisp。Common Lisp 的对象系统叫做 CLOS, CLOS 支持一种叫 MOP (Metaobject Protocol)的技术。用户可以通过 MOP 修改 class和 object 很多行为,比如怎么分配 slot,怎么继承 superclass,怎么决定 class precedence list等。那有了 CLOS+MOP,我能表达所有抽象,岂不美哉?

非也!CLOS 太过强大,要表达一些受限的抽象反而容易造成混乱。比如我用 CLOS 去表达 GTK 的 GObject 对象系统。

GObject 只支持单继承,而 CLOS 默认支持多继承。那我的抽象是否需要支持多继承?这时 候库的开发者就陷入了两难境地。支持多继承,如果用户要同时继承多个 GObject 对象怎 么办,我应该听谁指挥?甚至不同的父类还会有不同的 metaclass,用户还必须指定应该用 哪个 metaclass。不支持多继承,用户就得留着心眼看某些 class 是否支持多继承,与 CLOS 的『直觉』相违悖。

CLOS 的 MOP 当然可以『解决』这个问题,只要用 MOP 修改类的行为,解决掉多继承的冲 突,那么又可以快乐地多继承了… 但是,我并不认为这是一个好的抽象,因为为了使用抽象 (从 GObject class 里继承),用户被迫陷入琐碎的实现细节中(研究父类的 metaclass)。

做加法容易,做减法难。C 可能表达能力比较欠缺,实现某个抽象。写起来各种函数指针乱 飞,比较丑。但 C++ 为了实现某个抽象用上了 template,class 继承这些高级特性。却容 易在暗中留下坑,等着无知的程序员掉进坑里

另一个我想吐槽的事情,就是‘超市买菜’模型。很多人在设计语言的时候,都会有如下的思 路:‘X是好的,Y是好的,Z是好的,我也刚好都需要,所以我这个语言要有X, Y, Z。’,比 如上面那个人,就认为‘FP是好的,OOP是好的,我都需要,于是我去设计语法去了’

这样做的问题是完全没考虑到各种feature之间的互相影响-而这才是设计语言最麻烦的地 方。

-- 《超市买菜》

10 个赞

cireu 回顾历史上已有的经典的讨论过的案例,是学习如何 trade-off 的好办法。

上面有的朋友说,这种辩论可以引发论点对撞,激发头脑风暴,获取新的知识。但我觉得在 『language holy war』里面,『越辩越明』只是一厢情愿,很多的时候只是越辩越乱。

『没有银弹』这个道理太简短,大家都会背,都会说。但是实际要理解这个道理,需要很多 的实战经验支持。我觉得像 finalpatch 这些参与讨论的大佬,应该是明白这些道理的。但 是旁边围观的人不一定明白。而大佬们的时间往往比较宝贵,不想把时间浪费在这种琐碎的 事情上(我发这几个回复,整理资料组织语言都用了 1 个多小时)。如果没有通俗地讲解 问题,旁观的人不明所以,可能会被讨论的情绪所煽动,火上浇油把『激烈的技术讨论』变成『互相 攻击的骂战』。就像这个帖子里面的一样:

1 个赞

举例问题#

Common Lisp 没有逼你一定要 CLOS,因为考虑兼容 Lisp Machine 之类的历史原因结合得并不是很紧密,很多实现就是做成“没有 OOP 的 Lisp”以后再用 Lisp 实现 CLOS。实际上哪怕是直接从 OOP 自举的 SICL,也不影响用户自己另外实现 OOP 系统。

比如 200 行左右就能实现的 Corbits (CMU AI REPO)

显然更显著的问题是靠动态分发实现 OOP 的性能损失。但这个和问题没关系,因为 GObject 也有这问题。而且和其他所有性能问题一样都可以“靠编译器技术的进步”解决。

这里 C++ 的问题实际是,大约 2015 以后委员会就有了“C++不再是C的超集”的共识。只要标准继续更新,就会离兼容 C 越来越远,每次新标准出来都有更加魔幻的 C++ Hello World。反而一些老资历的程序员都没有意识到这点。

而不是像 ObjC,那才是“真的一直都兼容 C”。

3 个赞

不能编辑原想法,只好删了重发。。。


c和c++都喜欢。仅仅谈c语言的优点。

c从一开始就是作为高级的汇编存在,c的定位很明确,用c写的项目,多是底层稳定的模块,如nginx, redis, libev, kcp等等。

c的项目,虽然指针,宏等看起来并不清晰,但是这是c作为 ‘高级的汇编’ 不太优雅但是适用的解决方法。

如果为其做加法,例如: 增加泛型简化宏, 增加闭包简化void* args + void(func)(void)等高级语言的特性. 功能当然会更加强大,例如,上面道友提到的例子,c++的std::sort()可以内联函数对象,而c的qsort()要通过函数指针而多了一次函数调用。

不过如果这样做了,c的设计定位不再是‘高级的汇编’,语言逻辑出现漏洞,往后就只能不断打补丁了。

c一开始就坦然面对自己的缺点,虽然远远没有c++强大,但是拥有明确的语义和使用场景(也许在底层领域并不需要多么高级的语言特性)。 malloc, free等等都很明确,没有歧义,我认为这很重要!!!

我不太认同 “c++包含c,所以写c代码就是c++代码”,因为我认为语言的设计哲学会影响使用者的思维。例如使用c++时,一方面c++能够让你控制内存,但又允许你利用容器去接管内存,这样容易留下暗坑,如下代码(你不能像写java一样写的飞起)。而写c代码,你一开始就不会有这种想法。

std::vector<std::string> vec;
vec.push_back("hello");
std::string &str = vec[0];
v.push_back("world");

我发现你这个辩论方法很有意思:“我虽然打不过你,但是泰森打得过你,所以你也没什么了不起”。我从第一个帖子里就在说,我们用C++不是因为它是一种优美的语言,而是因为往往没有其他选择。做业余项目的朋友也许难以理解,在公司里做东西,不是我想今天加个kotlin或者rust就可以的。

这个帖子争论的开始就是我建议某个网友不要自找麻烦去用C做泛型容器,因为这个早就证明是做不好的。我看大家也都认可Go接受泛型以后变成了一种更好的语言。不知道为什么接受C语言不支持Generics是个缺点就这么困难,一定要争辩没有Generics用宏也都能做到一摸一样的效果。

2 个赞

你的观点没错,但是编程语言这个话题,大家自己工作经验,适用范围和个人喜好都不一样,更不用说编程语言的优缺点了。

这也是每次争论编程语言都不会有好结论的原因。

你的头像看起来很权威,我也学一下

7 个赞

我c,c++都喜欢,各自有适合的场景,不是竞争关系

哈哈哈哈,头像不错哈。

头像草

2 个赞

我感觉 cireu 并不是在和你辩论。分享那篇文章只是恰好联想到了而已,并非用来说 C/C++/Rust 哪个更好。 :grin:

是否被证明过我不清楚,但是我觉得你说的是有道理的。

对我来说,如果你说 “C语言不支持Generics” 那么我表示同意。我回想了一下,我之所以会回复,是因为看到下面这句。

特别是这里限定了“在性能上”,我想着只考虑性能的话,这句话有些绝对了。而且我当时说的是“类似的效果”,而不是“一模一样”。哈哈,你看这不小心错了一个词,意思就完全不一样了。当然我知道你肯定不是故意的 :grin: 。另外,咱们后面讨论的范围已经超出性能了,我感觉你的一些观点我是很赞成的。

下面有些题外话,昨天通过 @cireu 的帖子看到了很多之前未曾仔细读过的论战,以及翻到了论坛上的一些陈年旧事。有些感慨,在网络上做到有意义的讨论是一件比较困难的事情。 原因很多,其中一个是文字的表达力不足,举例来说:

  • 我发了一句:“今天上海天气真好”

你并不知道我这句是开开心心地说,还是咬牙切齿地说,又或者是充满讥讽地说。 :rofl:

而且大家的关注点又不一样,于是会有不同的回复:

  • A:难道昨天天气不好吗?
  • B:今天是个 XXX 纪念日,你就只顾着天气?
  • C:怎么着,上海还有优越感了?

而且每个人的主观感受也不同,心情可能完全不一样:

  • D:上海这么热的天气也叫好?
  • E:上海这么冷的天气也叫好?
  • F:(刚和别人吵过架,应该也会有不同的回复吧,我想不出例子了 :joy:

上面这些情况夹杂在一起,非常容易争论起来。尤其争论过程中,当有人不小心说了些让其他人觉得被伤害的话,而感觉到被伤害的人又要奋起反击的时候,场面可能会变得不可收拾。

这方面我自己的经验是,以最大的善意去揣测别人的回复,避免一些不必要的冲突。不一定适用于所有人,但执行下来确实能让我自己想得开了 :slight_smile:

上面这小段有些偏题了,不过初衷还是希望论坛能越来越好 :stuck_out_tongue_winking_eye:

我觉得有必要再加一句:我这些表情真的真的都是字面意思。自从微笑表情有了多种含义,感觉发表情也不安全了 :rofl:

5 个赞

老实讲我压根没想到有人会来杠用预处理宏实现泛型容器,因为这个选择太过不实际。结果这个讨论就变成了证明宏是不是一个可靠的语言机制,我当然觉得它很明显不是。

也不算是杠吧 :joy: ,因为看到在 Linux 内核里用宏很多,所以你一说泛型我就想到了 Linux 他们的写法,导致认为用宏是一个很正常的想法。至于选择是否实际,每个人看法可能会不同,我尊重你的想法。

想体验用宏过度可以看看 emacs 的 C 源代码(

5 个赞

笑死 :joy_cat:

DEFUN绝对是良心,那些CAR之类的一路跳进去简直想死。。 :rofl:

今天在Emacs中,用C语言实现了栈
感觉很好 :grinning:

1 个赞