Skip to content

Commit

Permalink
add zig and c++
Browse files Browse the repository at this point in the history
  • Loading branch information
xnhp0320 committed Apr 6, 2024
1 parent 3b6113c commit 08ce570
Showing 1 changed file with 156 additions and 0 deletions.
156 changes: 156 additions & 0 deletions content/post/2024-04-06-zig-c++.md
Original file line number Diff line number Diff line change
@@ -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<T>`,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 <typename InputType>
struct Some {
using OutputType = ...
}
```
相比之下, Zig直观太多。那么很自然的,计算一个类型,Zig里就是调用函数,而C++则是模板类实例化,然后访问类成员。
```c++
Some<InputType>::OutputType
```

相当于对于InputType调用一个Some“函数”,然后输出一个OutputType。

# Zig Comptime是命令式,而C++是模式匹配+递归

比如实现一个函数,输入一个bool值,根据bool值,如果为真,那么输出type A,如果为假那么输出type B。

```c++
//基本模式
template <bool, typename A, typename B>
struct Fn {
using OutputType = A;
};

//特例化的模式
template<typename A, typename B>
struct Fn<false, A, B> {
using OutputType = B;
};
```
从这里C++代码可以感觉出,其实你是拿着尺子,对照着基础模式,然后通过模版偏特化来实现一种`if-else语句`。
```c++
Fn<sizeof(A) > 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 <int Index, typename ...T>
struct Fn;
```

然后我们递归的基础情况

```c++
template <typename Head, typename ...Tail>
struct Fn<0, Head, Tail...> {
using Output = Head;
};
```
然后写递归式,
```c++
template<int Index, typename Head, typename ...Tail>
struct Fn<Index, Head, Tail...> : public Fn<Index - 1, Tail...>
{
};
```

这个地方其实稍微有点难理解,其实就是拿着`...T`来模式匹配`Head, ...Tail`

第一个偏特化,如果用命令式,类似于,

```c++
if (Index == 0)
return Head;
```

第二个偏特化,类似于

```c++
else {
return Fn(Tail...);
}
```

这里利用的其实是继承,让模板推导一路继承下去,如果Index不等于0,那么`Fn<Index, ...>`类其实是空类,即,我们无法继承到`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++,达到举一反三的效果。

0 comments on commit 08ce570

Please sign in to comment.