-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
164 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
--- | ||
title: "通过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>::OutputTypeC++ | ||
``` | ||
|
||
相当于对于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; | ||
} | ||
``` | ||
|
||
我们再来看递归的列子。比如有一个类型的list,我们需要返回其中第N个type。同样,由于在C++中,类型不是一等成员,因此我们不可能有一个`vector<type>`的东东。那怎么办呢?方法就是直接把`type list`放在模板的参数列表里:`typename ...T`。 | ||
|
||
于是,我们写出“函数原型”。 | ||
|
||
```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(Index-1, 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]; | ||
} | ||
pub fn main() void { | ||
const type_list:[]const type = &[_]type{u8, u16, u32, u64}; | ||
std.debug.print("{}\n", .{ chooseN(2, type_list)} ); | ||
} | ||
``` | ||
|
||
即这个也是完全命令式的。当然C++20之后也出现了`if constexpr`和`concept`来进一步简化模版元编程,C++的元编程也在向命令式的方向进化。 | ||
|
||
# 结束语 | ||
|
||
尽管Zig目前“还不成熟”,但是学习Zig,如果采用一种对照的思路,偶尔也会“触类旁通”C++,达到举一反三的效果。 |