写点什么

C++ 语法糟透了,Carbon 修复了它

  • 2022-08-23
    北京
  • 本文字数:4609 字

    阅读完需:约 15 分钟

C++ 语法糟透了,Carbon 修复了它

本文最初发布于 ITNEXT


我在 Twitter 上读到过许多关于 Carbon 的评论,有许多 C++开发人员对 Carbon 这门编程语言的语法很不满意。人们反复提到的一个问题是:


如果他们是要为 C++开发人员开发一门新语言,那为什么要让它看上去和 C++完全不同?C++的语法已经很好了,而且为人所熟知。


实际上不是这样的,C++的语法一点也不好。但是,一旦你在 C++领域呆了足够长的时间,就很容易将其内化而忽略这个事实。Chandler Carruth 在介绍Carbon的时候未能很好地说明为什么 C++语法多么问题重重。


虽然我不能保证能说明得很好,但我会试着挖下 C++语法的问题,以及为什么我们需要一个升级。让我们从一个简单的对比开始:



可以看到,Carbon 中的大部分语句都有一个引导关键字,如fnvarletclass。有了这些关键字,你一看就知道自己正在处理的语句是什么类型的。Carbon 是故意这样设计的。下面这句话来自Carbon的首次公开展示:


我不知道是否有人做过 C++解析器,那不是一个完整的编译器。其难度超乎想象。我们可以做得更好。—— Chandler Carruth


作为一名经常开发各种工具来辅助 C++开发的开发者,我完全同意这个说法。编写代码解析 C++难度非常大,这是我从 1998 年开始编写 C++代码以来的亲身感受。Java 和 C#社区有许多令人惊叹的 IDE 和工具,而我们 C++开发者多年来一直使用糟糕的工具进行开发,命令补全经常会出问题,尤其是在大型项目中。特别地,与 Java 和 C#开发人员可以使用的工具相比,C++的重构工具功能非常有限。


VS Code C++高亮显示器的维护者解释了为什么开发 C++工具如此之难:


C++语法高亮显示器有 19000 行代码,这比任何语言都要多,而且是第二大的将近 4 倍(Typescript 5000 行)。是不是很厉害?不是的,因为即便如此,它甚至还是无法高亮显示变量声明中的自定义类型。Rust 高亮显示器毫不费力就能做到这一点,因为它和 Carbon 非常像。

—— Jeffrey Hykin


不过,即便是 Java 和 C#,在解析方面也不是很理想,因为它们借用了许多 C/C++语法。我最先注意到使用引导关键字的好处还是在 Go 语言中。Go 在函数定义中引入了func 关键字,这使得代码搜索变得很容易,我可以更容易地分辨出是使用还是定义。下图分别展示了查找MassFlow 函数调用和查找MassFlow 函数定义:


C/C++语法的问题是,需要解析多个语言符号才能确定一个语句是什么。对于人类读者而言,这可能无关紧要,但对工具来说就并非如此了,编写一个正则表达来搜索什么东西,难度要大很多。

最棘手的分析


关于 C++代码的解析难度,有一个具体的例子是“最棘手的解析”。这个特别的术语最早是由 Scott Meyers 在其著作《Effective STL 中文版:50 条有效使用 STL 的经验》中提出的。下面这个例子可以说明这一问题:


// C++ most vexing parsevoid foo(double x) {  int bar(int(x));}
复制代码


第二行代码存在多义性。我们可以将其解释成如下所示的函数声明:


// A function named bar takes an integer and returns an integer.int bar(int x);
复制代码


C++允许在函数声明中用小括号把参数x括起来。这样,C++解析器就不好区分是声明bar 函数,还是声明一个变量bar ,并将x 值转为整型对其初始化。下面是一个更复杂点的例子:


// C++ Unnamed temporarystruct Timer {};
struct TimeKeeper { explicit TimeKeeper(Timer t); int getTime();};
int main() { TimeKeeper time_keeper(Timer()); return time_keeper.getTime();}
复制代码


main函数的第一行存在多义性:


TimeKeeper time_keeper(Timer());
复制代码


在 C++中,这一行看上去像定义了一个函数time_keeper ,返回TimeKeeper 对象,并接收一个函数对象作为参数。在 C++中声明函数时,可以不指定参数名。int foo(int); 是一个合法的函数签名。这种问题在 Carbon 中不会出现,原因如下:


  • Carbon 类没有构造函数

  • 对象通过赋值初始化


在下面的代码中,我们试着用 Carbon 重现了前面的 C++代码。我需要创建类函数Create 和Make 来代替 C++示例中使用的构造函数。


// Carbon class Timer {    fn Create() -> Self;};
class TimeKeeper { fn Make(t: Timer) -> Self; fn getTime[me: Self]() -> int;};
fn Main() -> i32 { let time_keeper: auto = TimeKeeper.Make(Timer.Create()); return time_keeper.get_time();}
复制代码


可以看到,Carbon 语法中有一些可能不太明显的东西。Self 是指封闭类的类型。方括号内的东西,如[me: Self] ,指任何没有显式传递的东西。那是诱发性的东西。在这种情况下,就相当于 C++开发者所熟悉的this 指针。Go 也有非常类似的方法。在 Go 中,getTime 方法将是下面这个样子:


// Go method examplefunc (me TimeKeeper) getTime() int
复制代码


方括号也可以用于其他隐式数据,比如类型参数。下面是 Carbon 代码的一个例子:


// Carbon code showing function parametersfn QuickSort[T:! Comparable & Movable](s: Slice(T)) {  if (s.Size() <= 1) {    return;  }  let p: i64 = Partition(s);  QuickSort(s[:p - 1]);  QuickSort(s[p + 1:]);}
复制代码


这里,我们没有指定this 对象,而是指定了一个类型参数T 。该参数必须满足Comparable 和Movable 接口。Carbon 使用单冒号: 来指定对象类型,而:! 则用于表示我们正在指定类型参数必须满足的接口。

解析 C++中的函数指针


当定义以函数为参数的高阶函数时,将返回类型放在前面而不是后面的问题就显现出来了。快速看下这段代码,然后告诉我,函数指针参数的名字是什么?


// C++int FindFirst(int xs[], int n, bool (*condition)(int x)) {    for(int i = 0; i < n; ++i) {        if (condition(xs[i])) {            return i;        }    }    return -1;}
复制代码


函数指针参数的名字是conditon 。我相信大多数人都会同意,这不是很快就可以读出来的东西。这体现了 C++语法的其中一个问题。我本打算大张旗鼓地展示下 Carbon 做得有多好,但很遗憾,Carbon 语言规范中没有任何地方提到函数指针。取而代之,我将展示下在 Go 语言中会是什么样子,并推测在 Carbon 中可以如何做。


// Gofunc FindFirst(xs []int, condition func(int) bool) int {  for i, x := range xs {    if condition(x) {      return i    }  }  return -1}
复制代码


Go 代码更容易阅读,这是因为参数名在前,而类型在后。它具有一致性。使用 Carbon,我认为代码会更清晰,因为它使用冒号来分隔类型和参数名:


// Carbon - Assumption (similar to Rust)fn FindFirst(xs: Slice(int), condition: fn(int) -> bool) -> i32
复制代码


实际上,这几乎就是使用 Rust 编写FindFirst 函数签名的方式。

摆脱常量引用混乱

在编写 C++代码时,其中最恼人的一件事就是处理常量正确性和引用。考虑下面的几何学示例代码。


// C++ classclass Circle {     public:    const Point& center() const;    float radius() const;        void  setCenter(const Point& pos);    void  setRadius(float radius);                bool inside(const Point& p) const;    bool intersect(const Circle& c) const;    private:    Point center;    float radius;};
复制代码


有时候你需要传递一个参数,比如圆的中心,一个常量引用const Point& pos ,但其他时候你不想这样做。我将半径作为一个float 副本来传递。从机器码层面考虑,那会更高效,因为单精度浮点数值可以通过一个微处理机寄存器来传递。而常量引用被实现为指针,也就是说,要获得实际需要的值就得额外进行一次间接取值。Carbon 完全解决了这个问题,它让编译器来确定怎么做最好。在 Carbon 中,上面的类可以实现为:


// Carbon classclass Circle {         fn center[me: Self]() -> Point;    fn radius[me: Self]() -> f32;        fn setCenter[addr me: Self*](pos: Point);    fn setRadius[addr me: Self*](radius: f32);                fn inside[me: Self](p: Point);    fn intersect[me: Self](c: Circle);        var center: Point;    var radius: float;}
复制代码


可以看到,setCenter 和setRadius 类似。Carbon 将找出把参数pos 和radius 传递给相应函数的最优方法,需要担心引用和常量。在 Carbon 中,参数默认是let类型的,因此它们本质上和常量类似。


有时,你确实会需要在调用者中修改一个值。Carbon 没有引用,因此你需要使用指针来代替。Self* 是指me 参数是一个指针,我们可以修改me 。与使用引用相比,使用指针的一个问题是需要获取对象地址。


// Carbon - Calling setRadius if it was defined as // fn setRadius[me: Self*](radius: f32)
var circle: Circle = MakeRandomCircle();var ptr: Circle* = &circle;ptr.setRadius(5);
复制代码


每次都使用这个地址来修改一个 Circle 对象很麻烦。出于这个原因,我们在me 前面加了addr关键字,让 Carbon 为我们获取地址。这就是为什么我们不获取地址就可以调用setRadius


// Carbon var circle: Circle = MakeRandomCircle();circle.setRadius(5);
复制代码

为代码可读性而设计


大多数开发人员从不研究或关心可用性和用户界面设计。实际上他们应该关心的,因为应用于用户界面布局的原则与编写清晰易读的代码有很多相通之处。


有一个重要的细节需要注意,那就是人类阅读文本的方式。只有当你还是个学习读写的孩子时,才会阅读单词中的单个字母。成年人通过形状来阅读单词,我们通过观察单词的形状来识别它们。


这意味着要快速识别不同的选择,单词和句子的形状应该不同。让我们通过一个例子来说明这个问题。考虑一个包含一系列选项的网页:

I want to customize tools...I want to have custom shows...I want to do custom animations...
复制代码


这是一个糟糕的设计,因为句子的形状相似度太高了,增加了视觉扫描的难度。我们可以改进下这个设计,把重复的部分提取出来:


I want to:  Customize tools...  Custom shows...  Custom animations...
复制代码


虽然有改进,但还是有问题,每个选项的第一个单词看上去还是非常相似。修改每个选项的措辞,会更容易阅读:


I want to do:  Tool Customizing...  Reset Shows...  Custom Animations...
复制代码


这些原则是如何应用到 Carbon 编程语言中的呢?Carbon 使用了简短的引导关键字,如fn ,让你可以把更多的注意力放在单个的方法或函数上:


// Carbon function names line upfn center[me: Self]() -> Point;fn radius[me: Self]() -> f32;    fn setCenter[addr me: Self*](pos: Point);fn setRadius[addr me: Self*](radius: f32);          fn inside[me: Self](p: Point);fn intersect[me: Self](c: Circle);
复制代码


这使得在 Carbon 中浏览一个函数或方法列表更快捷。像 Java 这样的语言在这方面很糟糕。我们首先读到的是public static void ,到最后才看到最重要的部分——函数名。C++好一点,但把返回值信息放在函数名之前还是会分散注意力:


// C++ function names don't line upconst Point& center() const;float radius() const;
复制代码


返回类型的名称不一样,方法名就无法对齐,浏览方法名列表就难一些。这也适用于变量列表。借助像 var 这样的引导关键词,就可以保证每一行的变量名都从相同的位置开始,读者就可以更快地浏览代码。


// Carbon variable names line upvar center: Point;var radius: float;
复制代码

小结


不管是就解析器方面,还是从开发人员阅读代码和使用工具角度来说,Carbon 的语法都更简单。Carbon 代码更便于开发人员搜索和浏览,因为有效位(如标识符)是从同样的字符偏移开始的。


原文链接:


C++ Syntax Sucks and Carbon Fixes It

2022-08-23 15:3914563

评论 3 条评论

发布
用户头像
“Rust 高亮显示器毫不费力就能做到这一点,因为它和 Carbon 非常像。” Rust比Carbon早多了好吧!
Carbon的目标不是现代C++的继任者吗?总要有点现代C++的影子,而不是为了语法解析器设计语法,没有了构造函数与C++混合编码并不会容易很多吧,大概又会引入golang的约定大于语法。
2022-08-29 10:03 · 上海
回复
用户头像
Carbon是C++的语法糖?
2022-08-27 13:31 · 北京
回复
用户头像
这个一看就。。。。不太能接受。。。
2022-08-23 19:35 · 浙江
回复
没有更多了
发现更多内容

进阶神册!Redis+Nginx+设计模式+Spring全家桶+Dubbo核心技术笔记

程序知音

Java 数据库 spring java架构 Java进阶

踢碎破局陷阱,来一场酣畅淋漓的 SLG!

网易智企

AI AIGC

GreptimeDB 使用指南|快速查询分析外部数据

Greptime 格睿科技

数据库 分布式 云原生 时序数据库 使用指南

利用透明压缩技术解决企业级SSD读写延迟挑战

ScaleFlux

压缩算法 固态硬盘 企业存储

论数字化大趋势下,建设财务共享中心的重要性

用友BIP

财务共享

水泥行业全球第一企业怎么进行财务共享建设?

用友BIP

财务共享

Last Week in Milvus

Zilliz

非结构化数据 Milvus Zilliz 向量数据库

点云标注技术推动该领域的发展

来自四九城儿

详解RocketMQ 顺序消费机制

华为云开发者联盟

后端 开发 华为云 华为云开发者联盟 企业号 5 月 PK 榜

商业地产研策如何搜集数据​

MobTech袤博科技

软件测试/测试开发丨Web自动化测试高级定位xpath

测试人

程序员 软件测试 自动化测试 测试开发

Wallys/DR5018+QCN6122/support for the latest Wi-Fi standards in networking devices.

Cindy-wallys

ipq5018 QCN6102 QCN6122

通过SQL获取每个月第n周任意天的数据

搞大屏的小北

sql查询 sql 第一周 每个月 周一

项目很大,是否忍一下?

巨梦科技

微前端

用友BIP新一代全球司库,重塑企业资金管理新价值

用友BIP

全球司库

怎样才能让业财融合真正为企业数智化转型起到推动作用?

用友BIP

财务共享

西南财经大学李玉周:数智化技术广泛使用推动管理会计加快落地

用友BIP

智能会计 价值财务

多元办公场景下,企业如何保障工作效率与数据安全流通?

人称T客

AIGC下的低代码赛道,你我皆是拓荒人

引迈信息

AI 低代码 AIGC JNPF

LangChain 查询使用指「北」

Zilliz

Milvus AIGC 向量数据库 zillizcloud langchain

MobTech MobLink|小程序、网页跳转App的原理

MobTech袤博科技

「ACL 2023」:火山语音团队多篇论文中选,涉多方向技术创新突破

科技热闻

财务共享五大价值助力央企构建世界一流财务管理体系

用友BIP

财务共享

怎样将图片直接转换为3d模型?

真大的脸盆

Mac Mac 软件 图片转换工具 图片转换模型软件

旅游景区如何寻找共享电单车厂家

共享电单车厂家

共享电动车厂家 景区共享电单车 共享电动车投放 景区共享电动车

inBuilder今日分享丨表单设计器画布渲染引擎揭秘

inBuilder低代码平台

财务标准化建设进程中,财务共享能起到什么作用?

用友BIP

财务共享

开源大模型新SOTA!支持免费商用,比LLaMA65B小但更强,基于1万亿token

Openlab_cosmoplat

开源社区 falcon

数据可视化:分布类可视化图表大全

2D3D前端可视化开发

大数据 数据分析 数字化转型 数据可视化 数据可视化工具

MegEngine 使用小技巧:如何解读 MegCC 编译模型几个阶段 Pass 的作用

MegEngineBot

深度学习 编译器 MegEngine Pass

AI浪潮再掀低代码开发热,快来了解最新趋势!

加入高科技仿生人

人工智能 低代码 AI技术

C++ 语法糟透了,Carbon 修复了它_语言 & 开发_Erik Engheim_InfoQ精选文章