From 08ce570928747beeaee3d87138aa76c94f33e621 Mon Sep 17 00:00:00 2001 From: Peng He Date: Sat, 6 Apr 2024 21:58:50 +0800 Subject: [PATCH] add zig and c++ --- content/post/2024-04-06-zig-c++.md | 156 +++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 content/post/2024-04-06-zig-c++.md diff --git a/content/post/2024-04-06-zig-c++.md b/content/post/2024-04-06-zig-c++.md new file mode 100644 index 0000000..1c222d8 --- /dev/null +++ b/content/post/2024-04-06-zig-c++.md @@ -0,0 +1,156 @@ +--- +title: "2024 04 06 Zig与C++" +author: Peng He +date: 2024-04-06T19:57:49+08:00 +--- + +尽管Zig社区宣称Zig语言是一个更好的C (better C),但是我个人在学习Zig语言时经常会“触类旁通”C++。在这里列举一些例子来说明我的一些体会,可能会有一些不正确的地方,欢迎批评指正。 + +# Zig和C++,“元能力” vs “元类型” + +在我看来,C++的增强方式是希望赋予语言一种“元能力”,能够让人重新发明新的类型,使得使用C++的程序员使用自定义的类型,进行一种类似于“领域内语言”(DSL)编程。一个通常的说法就是C++中任何类型定义都像是在模仿基本类型`int`。比如我们有一种类型T,那么我们则需要定义T在以下几种使用场景的行为: + +```C++ +T x; //构造 +T x = y; //隐式拷贝构造 +T x{y}; //显示拷贝构造 +x = y; //x的类型是T,复制运算符重载,当然也有可能是移动运算符重载。 +//以及一大堆其他行为,比如析构等等。 +``` + +通过定义各种行为,程序员可以用C++去模拟基础类型`int`,自定义的创造新类型。但是Zig却采取了另一条路,这里我觉得Zig的取舍挺有意思,即它剥夺了程序员定义新类型的能力,只遵循C的思路,即`struct`就是`struct`,他和`int`就是不一样的,没有必要通过各种运算符重载来制造一种“幻觉”,模拟`int`。相反,Zig吸收现代语言中最有用的“元类型”,比如`slice`,`tuple`,`tagged union`等作为语言内置的基本类型,从这一点上对C进行增强。虽然这样降低了语言的表现力,但是却简化了设计,降低了“心智负担”。 + +比如Zig里的`tuple`,C++里也有`std::tuple`。当然,`std::tuple`是通过一系列的模板元编程的方式实现的,但是这个在Zig里是内置的,因此写代码时出现语法错误,Zig可以直接告诉你是`tuple`用的不对,但是C++则会打印很多错误。再比如`optional`,C++里也有`std::optinonal`,Zig里只用`?T`。C++里有`std::variant`,而Zig里有`tagged union`。当然我们可以说,C++因为具备了这种元能力,当语法不足够“甜”时,我们可以发明新的轮子,但是代价就是系统愈发的复杂。而Zig则持续保持简单。 + +不过话说回来,很多底层系统的开发需求往往和这种类型系统的构建相悖,比如如果你的类型就是一个`int`的封装,那么即使发生拷贝你也无所谓性能开销。但是如果是一个`struct`,那么通常情况下,你会比较care拷贝,而可能考虑“移动”之类的手段。这个时候各种C++的提供的幻觉,就成了程序员开发的绊脚石,经常你需要分析一段C++表达式里到底有没有发生拷贝,他是左值还是右值,其实你在写C语言的时候也很少去考虑了这些,你在Zig里同样也不需要。 + +# Zig语言里,类型是一等成员 + +C语言最大弊病就是没有提供标准库,C++的标准库你要是能看懂,得具备相当的C++的语法知识,但是Zig的标准库几乎不需要文档就能看懂。这其实是因为,在C++里,类型不是一等成员(first class member),因此实现一些模版元编程算法特别不直观。但是在Zig里,`type`就是first class member,比如你可以写: + +```zig +const x: type = u32; +``` +即,把一个`type`当成一个变量使用。但是C++里如何来实现这一行代码呢?其实是如下。 + +```c++ +using x = uint32_t; +``` + +那么我们如果要对某个类型做个计算,比如组合一个新类型,Zig里其实非常直观 + +```Zig +fn Some(comptime InputType: type) type +``` + +即输入一个类型,输出一个新类型,那么C++里对应的东西是啥呢? + +```c++ +template +struct Some { + using OutputType = ... +} +``` + +相比之下, Zig直观太多。那么很自然的,计算一个类型,Zig里就是调用函数,而C++则是模板类实例化,然后访问类成员。 + +```c++ +Some::OutputType +``` + +相当于对于InputType调用一个Some“函数”,然后输出一个OutputType。 + +# Zig Comptime是命令式,而C++是模式匹配+递归 + +比如实现一个函数,输入一个bool值,根据bool值,如果为真,那么输出type A,如果为假那么输出type B。 + +```c++ +//基本模式 +template +struct Fn { + using OutputType = A; +}; + +//特例化的模式 +template +struct Fn { + using OutputType = B; +}; +``` +从这里C++代码可以感觉出,其实你是拿着尺子,对照着基础模式,然后通过模版偏特化来实现一种`if-else语句`。 + +```c++ +Fn sizeof(B), A, B>::OutputType +``` +这就是比较类型的size大小,如果A大,OutputType就是A,如果B大,OutputType就是B。 + +如果用Zig来做,则完全是命令式的 + +```Zig +fn Fn(comptime A:type, comptime B: type) type { + if (@sizeOf(A) > @sizeOf(B)) { + return A; + } + return B; +} +``` + +我们再来看递归的列子。比如有一堆类型,`typename ...T`,在C++里,我们如何实现一个方法返回第N个类型? + +首先,我们写出“函数原型”。 + +```c++ +template +struct Fn; +``` + +然后我们递归的基础情况 + +```c++ +template +struct Fn<0, Head, Tail...> { + using Output = Head; +}; +``` + +然后写递归式, + +```c++ +template +struct Fn : public Fn +{ +}; +``` + +这个地方其实稍微有点难理解,其实就是拿着`...T`来模式匹配`Head, ...Tail`。 + +第一个偏特化,如果用命令式,类似于, + +```c++ +if (Index == 0) + return Head; +``` + +第二个偏特化,类似于 + +```c++ +else { + return Fn(Tail...); +} +``` + +这里利用的其实是继承,让模板推导一路继承下去,如果Index不等于0,那么`Fn`类其实是空类,即,我们无法继承到`using Output`的这个`Output`。但是index总会等于0,那么到了等于0的那天,递归就终止了,因为,我们不需要继续Index - 1下去了,编译器会选择特化好的`Fn<0, T,Tail...>`这个特化,而不会选择继续递归。 + +但是Zig实现这个也很直观,由于`slice`和`type`都是内置的,我们可以直接: + +```Zig +fn chooseN(N:u32, comptime type_list:[]const type) type { + return type_list[N]; +} +``` +即这个也是完全命令式的。当然c++20之后也出现了`if constexpr`和`concept`来进一步简化模版元编程,也在向命令式的方向进化。 + + +# 结束语 + +尽管Zig目前“还不成熟”,但是学习Zig,如果采用一种对照的思路,偶尔也会“触类旁通”C++,达到举一反三的效果。 \ No newline at end of file