Skip to content

Instantly share code, notes, and snippets.

@KmolYuan
Last active May 10, 2024 09:48
Show Gist options
  • Save KmolYuan/e8f396c54581c4676d238d3490d4387f to your computer and use it in GitHub Desktop.
Save KmolYuan/e8f396c54581c4676d238d3490d4387f to your computer and use it in GitHub Desktop.
C/Rust 類型對應表

C/Rust 類型對應表

1 byte = 8 bit,sizeof (C/C++) 和 {std, core}::mem::size_of (Rust) 回傳的是 bytes。

sizeof(double) // 8
std::mem::size_of::<f64>() // 8

布林

限於記憶體最小規劃為 bytes,二元值的實作為 8 bit 而非 1 bit。

C C++ Rust
bool(stdbool.h)/_Bool bool bool
  • Rust 的 bool 類型僅能轉換至整數類型,不可直接轉型至浮點數。反之,其它類型要透過判斷式變成布林,不能直接放在 if 語句中。

整數

C Rust
unsigned char char u8 i8
unsigned short short u16 i16
unsigned+(int/long) int/long/long int u32/⚠️char i32
unsigned long long long long u64 i64
u128 i128
size_t ssize_t usize isize
  • Rust 的字元類型 char 為 UTF-8 編碼,因此實作上使用 u32 而非 u8 (ASCII 編碼)。
  • 指標 (pointer) 的大小與 size 類型相同,依作業系統而定。32 位元為 4 bytes (32 bit),64 位元為 8 bytes (64 bit)。

浮點數

C Rust
float f32
long double/double f64
(外部套件提供)f128

靜態容器

Rust 擁有靜態容器,方便多變數的儲存,而不需要自己定義類型。

  • 陣列 (array) 以連續記憶體儲存多個相同類型的資料,寫作 [T; N]。其中 T 是任意已知大小的類型,N 為陣列長度,必須是靜態 usize 數值。
  • 元組 (tuple) 以連續記憶體儲存多個不同或相同類型的資料,寫作 (T1, T2)。本質上可視為結構體,會有記憶體對齊,但是結構體是自己定義的類型。元組的欄位最高到 12 項支援自動 trait 的實作。

Zero-Sized Type (ZST)

零大小類型 (ZST) 不占用記憶體空間,但是可以用來做語意上的識別,其指標仍與其它指標一樣擁有大小。C 在規範上不允許空的結構體,而 C++ 會使用 1 byte 識別空的結構體。

在 Rust 的單元 (unit) 類型 ()、空結構體 struct、空枚舉 enum、永不 (Never) 類型 ! 都是 ZST,單元類型除了代表 C/C++ 的 void 語意以外,也與其它類型無異。如果要做語意上的區分,空結構體可以更加靈活,因為它屬於自己定義的類型,可以實作第三方 trait。最後,空枚舉和 ! 僅能作為類似「名稱空間 (namespace)」使用,因為它無法建立實體,但是仍然可以綁定無實體的靜態函式並被當成類型,並且被最佳化消除(如 Result<T, !> 是永不產生錯誤的類型)。

指標

  • 指標可視為 usize 的整數,但是為一個記憶體位址。
  • 指標 (Pointer) 可以指到記憶體位址,但是無法保證指向的位置有效(已分配記憶體)。
  • 參照 (Reference) 可以確保其指到有效位置。C++ 中位置無效時為未定義行為,Rust 透過生命週期參數 'a 保證一定有效。
C C++ Rust
生指標 (Raw Pointer) T* T const*/T* *const T/*mut T
參照 (Reference) N/A T const&/T& &'a T/&'a mut T
右值參照(移動語意) N/A T&& 指派 = 就是移動語意
智慧指標(自擁有指標) N/A std::unique_ptr<T> Box<T>/alloc::box::Box<T>(no-std)
  • 移動語意是指變數綁定 (bind) 在數據上,數據不限定分配在單一變數上,因此轉移到下個變數時不會產生複製。這種設計有利於編譯器最佳化,甚至不用實際移動記憶體位址。過往 C 語言默認指派行為是複製,因此必須分析變數傳遞的分支來簡化複製行為,若為無分支,才等同移動語意。
  • 自智慧指標 (Smart pointer) 可以當成長度為 1 的 Vec<T> 容器,指標可以跟一般的變數一樣傳遞,且可以產生深層複製(複製指向類型 T)。好處是保證指向類型不會移動,大小跟指標一樣,而且有自動釋放機制(透過物件導向的解構函式)。

切片 (Slice)

陣列指標 &[T; N] 可以描述連續記憶體的位址,但是當 N 為動態長度時,可以變成 &[T],稱為 Slice。可以將 Slice 視為一個結構體包含第一個元素的指標 &T 與動態長度 usize,以元組表示如 &[T] == (*const T, usize)&mut [T] == (*mut T, usize)

  • 動態長度的連續記憶體容器變成 Slice,例如 Vec<T> 變成 &[T]
  • 動態字串類型 String 的底層為 Vec<u8>,可以視為 &[u8],但是字串為 UTF-8 編碼,u8 無法單獨檢索,char (u32) 才可以。因此 &str 被用來描述編碼的字串 Slice。
  • 與字串 Slice &str 相同,原生平台的 &std::ffi::CStr 字串對應平台的編碼字串 std::ffi::CString

由於 Slice 類型是作為指標傳遞的,指向類型 [T]strCStr 為無大小類型 (Unsized Type),記作 !Sized trait(沒有實作 Sized)。使用上,只有 ?Sized 可以填入無大小類型和有大小類型。

動態特徵

動態特徵 (Dynamic Trait) 使用特徵描述實體。某些類型,例如函式,每個實體有不同的類型,使用時只需要用特徵的功能。這時 dyn Trait 語法使用兩個指標指向實體與其特徵的函式,可以在需要類型名稱的情況下傳遞與儲存,例如函式參數的類型與結構體欄位的類型,能用參考指標 &'a dyn Trait(生命週期 'a)或自擁有的指標 Box<dyn Trait>(靜態生命週期)。

  • Box<dyn Trait> 較為泛用,這樣實體能跟隨自擁有指標 Box 移動且方便修改,除非實體本身不能移動(會用 Pin<&mut dyn Trait>)。
  • dyn Trait + 'a 語法代表實體本身帶有生命週期變數,例如閉包函數捕捉了外部變數。實體的生命週期與攜帶的生命週期不同,從語法上可得知 &'a dyn Trait + 'b,設計上應盡量減少生命週期限制,使用靜態生命週期 'static

跟 Slice 一樣,動態特徵 dyn Trait 是無大小類型 !Sized

Reference Counter (RC) & Lock

C++ Rust
單執行緒 RC std::shared_ptr<T> std::rc::Rc<T>
跨執行緒 RC std::atomic<T> std::sync::Arc<T>/std::sync::atomic::Atomic*
單執行緒鎖 和跨執行緒共用 std::cell::Cell<T: Copy>/std::cell::RefCell<T: Clone>
跨執行緒鎖 std::mutex std::sync::Mutex<T>
跨執行緒讀寫鎖 std::shared_mutex std::sync::RwLock<T>
  • 引用計數器 (RC) 用來在單/多執行緒間分享相同資料,當計數為 0 時釋放資源。
  • 執行緒鎖用來避免資料競爭 (Data Race),但是可能有自鎖 (Dead lock) 行為。由於 RC 的內部類型通常不允許修改,因此要搭配執行緒鎖來保證寫入時沒有衝突。
  • C++ 的鎖不持有資料,並且要主動呼叫守衛 (guard) 類型來鎖定,須注意操作的對象是正確的。
  • Rust 有為原始類型提供原子操作,改善記憶體和操作速度。

可以將 Python/JavaScript 等腳本語言的 RC 行為理解為

a = 10  # 新資源
b = a  # 引用 +1
c = b  # 引用 +1
del a  # 引用 -1
b += 1  # 使用引用值運算,並改變引用到新資源
let a = Rc::new(10); // 新資源
let mut b = a.clone(); // 引用 +1
let c = b.clone(); // 引用 +1
println!("{}", Rc::strong_count(&c)); // 3
drop(a); // 引用 -1
println!("{}", Rc::strong_count(&c)); // 2
b = Rc::new(*b + 1); // 使用引用值運算,並改變引用到新資源
println!("{}", Rc::strong_count(&c)); // 1

記憶體布局 (Layout)

使用結構體 struct 或是元組 (T, U, V) 包裝類型時,申請記憶體必須以「最大連續記憶體」為倍數,剩下的空間會僅用做填充。舉例來說,(u8, u16) 的最大連續區塊是 u16,所以從 2 bytes 為基準,申請的空間為 4,而非單純兩者相加的 3 bytes。這個「最小單位」可以使用 alignof (C/C++) 和 {std, core}::mem::align_of (Rust) 查詢。對齊的最大單位與平台位元數相同,可從 usize 的大小得知。

struct A { char b; short i; };
alignof(struct A) // 2
std::mem::align_of::<(u8, u16)>() // 2

看起來像這樣:(若有兩個 u8 放前面就不會浪費空間了!)

1 2 3 4
u8 u16(1) u16(2)

在 C/C++ 結構體的成員 (member)(在 Rust 稱為欄位 (field))的順序就是記憶體的排序,然而如果大小交錯排列,像 (bool, u64, bool, u64),會造成嚴重的空間浪費,所以基本上應該按類型的記憶體大小排列成員。不過在 Rust 中會自動在實作時達成,這樣撰寫時能夠以其它方式排列欄位。

// C required
#include <stdbool.h>
#include <stdalign.h>

struct A { bool b1; bool b2; long i1; long i2; };
alignof(struct A) // 8
sizeof(struct A) // 24

struct A { bool b1; long i1; bool b2; long i2; };
alignof(struct A) // 8
sizeof(struct A) // 32
std::mem::align_of::<(bool, u64, bool, u64)>() // 8
std::mem::size_of::<(bool, u64, bool, u64)>() // 24

// 強制變成 C 的布局,取消自動排列
#[repr(C)]
struct A {
   a: u8,
   b: u64,
   c: u8,
   d: u64,
}
std::mem::size_of::<A>() // 32

若為巢狀結構,外層的結構體則取最大欄位的 align_of,但是實際大小 size_of 通常無法攤平對齊,需要仰賴編譯器最佳化(例如刪除無用的欄位)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment