0%

為了搞懂Rust Pin在做什麼,耗費了很多精力,還真是有夠難懂的

About Self-Reference Type

有個資料結構, 其中有個指標指向結構自己或是結構中的某個欄位
例如

1
2
3
4
5
6
7
8
9
struct Test {
protected:
std::string a_;
const std::string* b_;
public:
Test(std::string text) : a_(std::move(text)), b_(&a_) {}
const std::string& a() const { return a_; }
const std::string& b() const { return *b_; }
};

這裡的b_指向a_的地址, 同樣的事情在Rust寫成這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Test {
a: String,
b: *const String,
}

impl Test {
fn new(txt: &str) -> Self {
Test {
a: String::from(txt),
b: std::ptr::null(),
}
}

fn init(&mut self) {
let self_ref: *const String = &self.a;
self.b = self_ref;
}

fn a(&self) -> &str {
&self.a
}

fn b(&self) -> &String {
unsafe {&*(self.b)}
}
}

不看Rust的safe機制造成的不同,原理是相同的
現在的問題是,假設物件被移動了,指向結構中某部分的指標該怎麼辦
例如

1
std::swap(test1, test2);

先從我比較熟悉的C++來說好了

Solution1: Keep invariant

雖然達成目標的方法有很多,不過原則都是一樣:維持不變量就好了

1
2
3
4
void swap(Test& lhs, Test& rhs) {
std::swap(lhs.a_, rhs.a_);
}
swap(test1, test2);

很顯然,這個方法不適用於Rust

Solution2: Don’t move the object

所謂的Pin也就是這麼一回事,當物件停留在記憶體的某個位置之後,就不會再移動了,所以Self-Reference Type的物件,在生命週期結束之前,所有的pointer和reference都會有效
在C++禁止的方法也不只一種,這是方法之一

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
void swap(T&, T&) {}
struct Test {
protected:
std::string a_;
const std::string* b_;
public:
Test(std::string text) : a_(std::move(text)), b_(&a_) {}
friend void swap(Test&, Test&) = delete;
const std::string& a() const { return a_; }
const std::string& b() const { return *b_; }
};

不過由於Rust講究Safety,所以訂了一堆規則

About Pin in Rust

在Rust中對Self-Reference Type的處理,我們要禁止的只有這件事

1
2
3
4
5
6
7
pub fn swap<T>(x: &mut T, y: &mut T) {
// SAFETY: the raw pointers have been created from safe mutable references satisfying all the
// constraints on `ptr::swap_nonoverlapping_one`
unsafe {
ptr::swap_nonoverlapping_one(x, y);
}
}

禁止Rust拿到&mut T的Reference,&mut Tˊ自然是不行,Box<T>也做不到這件事,所以就是Pin<T>登場的時候

Rust的Type分成兩類:

  • Default Type:可以安全在Rust Move的類型
    • Default Type都實作了auto Unpin trait,也就是什麼都不用做
  • Self-Reference Type:也就是上面提到的部分
    • 必須實作!Unpin的部分
    • 使用PhantomPinned就可以了

以下是個範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
_marker: PhantomPinned,
}

impl Test {
fn new() -> Self {
Test {
_marker: PhantomPinned
}
}
}
pub fn main() {
let mut test1 = Box::pin(Test::new());
let mut test2 = Box::pin(Test::new());
// compile failed
std::mem::swap(test1.as_mut().get_mut(), test2.as_mut().get_mut());
}

你把上面的PhantomPinned註解掉,程式就能正常運作了
Pin還有很多細節,等我真的變成全職Rust工程師在研究吧

Reference

最近被吵得很兇的Safe C/C++,主要討論的是沒有Undefined Behavior這件事

What’s Undefined Behavior

基本上就是一個逃生艙口,Compiler可以跳過某些邏輯的推理,正確的邏輯永遠不會引發UB,不正確的邏輯(可能)會引發UB

舉個例子:

1
2
3
4
5
6
7
8
int f(bool init) {
    int a;
    if (init) {
        a = 6;
    }
    return a / 2;

}

觸犯了UB,所以gcc/clang開啟Optimization時會產生

1
2
3
f(bool):
mov eax, 3
ret

不過Undefined Behavior是Runtime Concept,所以以下的程式碼只有在Runtime才會發生UB

1
2
3
4
5
6
7
8
9
10
11
12
13
int nervous(bool is_scary, int n)
{
if (is_scary) {
return 100 / n;
} else {
return 0;
}
}

int main()
{
return nervous(false, 0);
}

所以Undefined Behavior Sanitizer只能在Runtime下使用t,也不能保證抓到所有UB,只要UB Code沒被執行到,整個程式行為還是受到標準限制的

How to archive safety

基本上分成兩個流派

  • 程式語言中本身就沒有Undefined operations,如Java,Python,不過也是犧牲了一部分的速度和超能力,沒有十全十美的
  • 將程式語言切分為Safe跟Unsafe的部分,如Rust
    • Safe的地方由Compiler保證,不會有任何UB發生
    • Unsafe的地方也沒有什麼特異功能,只是相信Unsafe的地方不會有任何問題

Future of safety C++

老實說,我不知道
Safecpp提供了一套類似於Rust的機制,不過那已經不算是C++了
Profile機制只打算完成80%的Safety,如果能達到這目標,我覺得夠用了,不過要說服NSA和美國政府是另一回事了

Reference

由於打算寫本電子書,所以重新審視了C++20 Moudle的部分
語法的不是這篇的重點,這篇講的是

  • 如何跟CMake搭配使用
  • 測試環境是Linux + Clang20 + CMake 3.28

Prerequisites

首先先clone git repo,所有的變化都由範例legacy開始,這是沒有Module之前的做法

CMake

CMake已經是事實上的標準

Case1 Normal case

詳細內容請觀看 module_1目錄,這邊只講我覺得重要的地方
這個Case就是legacy直接翻譯成Module版本
首先看MathFunctions的CMakeLists.txt的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
target_sources(MathFunctions
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
PRIVATE
FILE_SET implementaion_units
TYPE CXX_MODULES
FILES
src/mysqrt.cppm
PRIVATE
src/MathFunctions.cxx
)

這邊有兩個FILE_SET

  • primary_interface:也就是我們要對外提供的Primary module interface unit
  • implementaion_units:內部的partion unit,不對外輸出
    所以在安裝的時候,只會將MathFunctions.cppm複製到安裝的目錄下

    Case2: Multiple Primary Module Interface Units

    接著我們稍微修改MathFunctions.cppm的內容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    module;

    export module Math;

    export import :detail;
    export namespace mathfunctions
    {
    double sqrt(double);
    }
    我們也將detail的內容也輸出了,因此我們需要做以下的修改
  • detail module和namespace需要標記成 export
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    module;
    #include <math.h>

    export module Math:detail;

    export namespace mathfunctions::detail {
    double sqrt(double x) {
    return ::sqrt(x);
    }
    }
    修改我們的CMakeLists.txt的部分
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    target_sources(MathFunctions
    PUBLIC
    FILE_SET primary_interface
    TYPE CXX_MODULES
    FILES
    MathFunctions.cppm
    src/mysqrt.cppm
    PRIVATE
    src/MathFunctions.cxx
    )
    現在我們有了兩個Primary Module Interface Units,在安裝的時候也要同時複製兩個檔案
    Math.detailMath:detail的情況類似,所以就不說了

接著來研究Mitgrate的部分,這是參考clang Transitioning to modules的部分

Case3: Mitgrate legacy to module (Part1)

看一下transform_1的目錄
這邊主要的差別在於CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
target_sources(MathFunctions
PUBLIC
FILE_SET export_headers
TYPE HEADERS
BASE_DIRS include/
FILES include/MathFunctions.h
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
PRIVATE
src/MathFunctions.cxx
src/mysqrt.h
src/mysqrt.cxx
)
install(TARGETS MathFunctions
EXPORT MathFunctionsTargets
ARCHIVE
FILE_SET export_headers
FILE_SET primary_interface
DESTINATION lib/cmake/MathFunctions/src
)

既保留原有的leagcy code,更新增了一個Primary Module Interface Units
MathFunctions.cppm的內容則是

1
2
3
4
5
6
7
8
module;
#include "MathFunctions.h"

export module Math;

export namespace mathfunctions {
using mathfunctions::sqrt;
}

將Global Module Fragment中的內容導出到Module中
這種方法不會破壞原有leagcy code,殺傷力最小

Case4: Mitgrate legacy to module (Part2)

看一下transform_21的目錄,CMakeLists.txt跟上面一樣不變
改變的是MathFunctions.cppmMathFunctions.h
此時的MathFunctions.cppm長這樣

1
2
3
4
5
6
7
8
9
module;

export module Math;

#define IN_MODULE_INTERFACE

extern "C++" {
#include "MathFunctions.h"
}

MathFunctions.h的內容則是

1
2
3
4
5
6
7
8
9
10
11
#pragma once

#ifdef IN_MODULE_INTERFACE
#define EXPORT export
#else
#define EXPORT
#endif

namespace mathfunctions {
EXPORT double sqrt(double x);
}

由於只有在Module狀態下,IN_MODULE_INTERFACE才會發揮作用,因此leagcy code的情況下會維持不變
這個方法雖然比上面麻煩,不過可以順利遷移到下一個階段

Case5: Mitgrate legacy to module (Part3)

所有方案中最麻煩的一種
主要思想是在implemtation unit當中切開legacymodule的實作,強迫Consumer只能使用其中一種,例如原先的Header可能要加上export

1
2
3
4
5
6
7
8
9
10
11
#pragma once

#ifdef IN_MODULE_INTERFACE
#define EXPORT export
#else
#define EXPORT
#endif

namespace mathfunctions {
EXPORT double sqrt(double x);
}

以及Implementation的部分也要隔開

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef IN_MODULE_INTERFACE
#include "MathFunctions.h"
#include "mysqrt.h"
#else
module Math;
#endif

namespace mathfunctions {
double sqrt(double x) {
return detail::sqrt(x);
}
}

在這裡我選擇對CMakeLists.txt動手腳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if (ENABLE_MODULE_BUILD)
target_sources(MathFunctions
PUBLIC
FILE_SET export_headers
TYPE HEADERS
BASE_DIRS include/
FILES
include/MathFunctions.h
include/mysqrt.h
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
PRIVATE
src/MathFunctions.cxx
src/mysqrt.cxx
)
else()
target_sources(MathFunctions
PUBLIC
FILE_SET export_headers
TYPE HEADERS
BASE_DIRS include/
FILES
include/MathFunctions.h
PRIVATE
include/mysqrt.h
src/MathFunctions.cxx
src/mysqrt.cxx
)
endif()

target_compile_definitions(MathFunctions
PRIVATE
$<$<BOOL:${ENABLE_MODULE_BUILD}>:IN_MODULE_INTERFACE>
)

if (ENABLE_MODULE_BUILD)
install(TARGETS MathFunctions
EXPORT MathFunctionsTargets
ARCHIVE
FILE_SET export_headers
FILE_SET primary_interface
DESTINATION lib/cmake/MathFunctions/src
)
else()
install(TARGETS MathFunctions
EXPORT MathFunctionsTargets
ARCHIVE
FILE_SET export_headers
)
endif()

當我們指定ENABLE_MODULE_BUILD的時候,會自動處理細節的部分
不過這邊也遇到了clang文件中的問題

Minor issue

由於我們之前的mysqrt.h是經由src/MathFunctions.cxx所include的,改成Module之後,這個相依性被切斷了
因此我們需要在MathFunctions.cppm強迫加入

1
2
3
4
5
6
7
module;

export module Math;
#include "MathFunctions.h"

module: private;
#include "mysqrt.h"

這樣沒有問題,不過

  • 原來的mysqrt.h不需要公開,現在變成強迫要公開了
  • 更好的方法是直接使用Module Partition Unit,也就是要改寫

問題說來很簡單,結果我花了好久才搞懂,字字血淚
我想要的只有這樣的效果

1
2
3
4
5
6
7
8
template <std::size_t i>
void inner() {}

tempplate <std::size_t N>
void outer() {
for (std::size_t i = 0; i < N; i++)
inner<i>();
}

結果這樣是無法編譯的,只好找其他方法繞過

Solution 1: Template specialization

一樣是從別人的Code上學來的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <std::size_t i>
void inner() {}

template <std::size_t N>
struct helper {
    template <typename Indices = std::make_index_sequence<N>>
    struct _helper;

    template <std::size_t... Idx>
    struct _helper<std::index_sequence<Idx...>> {
        static void unroll() {
            (inner<Idx>(), ...);
        }
    };
};

template <std::size_t N>
void outer()
{
        helper<N>::template _helper<>::unroll();
}

在這邊

  • _helper會有兩個Template definition,而由於_helper<std::index_sequence<Idx...>>的契合度比primary template好,所以會選擇這個definition
  • 然後就用fold expression展開了
  • 不過實在是太醜了,思考有無其他的方案

    Fail attempt:

    在求助於AI之下,嘗試了其他方案

    std::apply and std::make_integer_sequence

    這個是Claude提供的
    1
    2
    3
    4
    5
    template <size_t i> void inner() {}
    template <size_t N>
    void outer() {
    std::apply([](auto... indices) { (inner<indices>(), ...); }, std::make_index_sequence<N>{});
    }
    結果是不能用,std::apply需要接受一個tuple like的參數,很可惜這不是
    於是先擱置在一旁,不過這個方案接近我最後的方案,還是感激一下AI

    Custom apply function

    這個是GPT4-o提供的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <typename F, size_t... Indices> 
    void apply_impl(F&& f, std::index_sequence<Indices...>) {
    (f(std::integral_constant<size_t, Indices>{}), ...);
    }
    struct outer {
    template <std::size_t N>
    void test() {
    apply_impl([](auto index) { inner<index>(); }, std::make_index_sequence<N>{});
    }
    };
    將fold expression隱藏於實作中,雖然不錯,不過這方法也是有其缺點
    無法表示這樣的Pseudo code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template <std::size_t i>
    bool inner() {
    if (i == 1) return false;
    else return true;
    }

    tempplate <std::size_t N>
    bool outer() {
    for (std::size_t i = 0; i < N; i++)
    if (!inner<i>()) return false;
    return true;
    }
    萬能的CharGPT也給了一個思路,拓展apply_impl
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template <typename F, size_t First, size_t... Rest>
    bool apply_impl(F&& f, std::index_sequence<First, Rest...>) {
    if (!f(std::integral_constant<size_t, First>{})) {
    return false;
    }
    return apply_impl(f, std::index_sequence<Rest...>{});
    }

    template <typename F>
    bool apply_impl(F&&, std::index_sequence<>) {
    return true;
    }
    看起來可行,不過也是有自己的問題
  • 當你邏輯改變,例如將all_true或城anyone_true的時候,上面的apply_impl就要改了
  • 當Callback function有無回傳值的情況,就要考慮很多地方,整個邏輯也會變得支離破碎,不好維護
    因此,想出了自己的方案

    Solution2: make_index_sequence_tuple

    查了一下資料std::apply接受的是tuple like的類型,那麼就自己做出一個tuple likeindex_sequqence就好了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template <std::size_t... Idx>
    constexpr auto _make_index_sequence_tuple(std::index_sequence<Idx...>)
    {
    return std::make_tuple(std::integral_constant<std::size_t, Idx>()...);
    }

    template <std::size_t N>
    constexpr auto make_index_sequence_tuple()
    {
    return _make_index_sequence_tuple(std::make_index_sequence<N>{});
    }
    struct outer {
    template <std::size_t N>
    void test() {
    std::apply([](auto... indices) {
    (inner<indices>(), ...); },
    make_index_sequence_tuple<N>()
    );
    }
    };
    這樣就沒啥問題了

    Conclusion

    AI生成的Source Code還是不能照單全收,不過提供個思路總是好的
    不過更好的方法應該是template for,不過不知會部會進標準

API Design

看了lexy的程式碼之後,對其API Design很有興趣
其中有一個設計

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template <typename Fn>
struct _parse_state : _detail::placeholder_base {
LEXY_EMPTY_MEMBER Fn _fn;
template <typename State, typename... Args>
constexpr decltype(auto) operator()(State& state, const _detail::tuple<Args...>&) const
{
static_assert(!std::is_same_v<State, _detail::no_bind_state>,
"lexy::parse_state requires that a state is passed to lexy::parse()");
return _detail::invoke(_fn, state);
}
}

template <>
struct _parse_state<void> : _detail::placeholder_base
{
template <typename State, typename... Args>
constexpr decltype(auto) operator()(State& state, const _detail::tuple<Args...>&) const
{
static_assert(!std::is_same_v<State, _detail::no_bind_state>,
"lexy::parse_state requires that a state is passed to lexy::parse()");
return state;
}

template <typename Fn>
constexpr auto map(Fn&& fn) const
{
return _parse_state<std::decay_t<Fn>>{{}, LEXY_FWD(fn)};
}
};

constexpr auto parse_state = _parse_state<void>{};

這API的重點

  • 我們只希望有一個Root parse_state
  • 只能透過 Util function map,可以創建另一個placeholder_base的subclass
    lexy採用的是Partial template specialization方案來應對這個設計,不過這方案有個缺點,大量重複的程式碼
    以上面的例子來看,operator()的內容幾乎一樣,如果功能更多的話,可能會有更多類似但卻不相同的函數出現,可能增加維護的難度

    deduce this

    deduce this的教學文章很多了,拾人牙慧也沒什麼意思
    雖然不確定是否是最佳解,不過可以試試看設計這樣的API
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct Base {
    template <typename Self>
    decltype(auto) operator()(this Self&& self) {
    // Shared common logic
    if constexpr (std::is_same_v<std::decay_t<Self>, Base>) {
    } else {
    }
    }
    template <typename Self>
    requires std::is_same_v<std::decay_t<Self>, Base>
    void test1(this Self&& self) {
    }
    };

    struct Derived : public Base {};
    constexpr auto parse_state = Base{};
    這邊的map跟lexy上面的map差不多
  • 只能透過parse_state來使用util function map
    operator()就能共用大部分的邏輯了
    不過這方法也是有缺點,對邊義氣版本的要求很高
  • gcc目前要14才有支援
  • clang目前也是到18才支持
  • MSVC至少要17.2

CRTP

在寫完後不久,才想起CRTP也可以解決這問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename Derived = void>
struct Base {
decltype(auto) operator()() const {
if constexpr (std::is_void_v<Derived>) {
static_assert(false);
} else {
}
}

void map() const
requires std::is_void_v<Derived> {
}
};

struct Derived {};

不過比起deduce this,可以要麻煩一點

Reference

其實這也不是什麼新玩意,不過由於AI和其他方面的需求加持,這東西越來越重要
先分析一下問題

Problem

假設我們想在程式裡面直接嵌入icon或是wave,該怎麼做

1
uint8_t icon[];

現在的問題,嵌入的content是Binary的,如何跟程式綁在一起

Solution 1: String Literal

使用類似xxd, objcopy或是類似的工具,將Binary轉成String Literal,然後跟原先的程式做聯結就行了
不過可能遇到的問題

  • xxd 或是其他的工具,該作業系統可能不能用或是要額外安裝,增加額外的複雜性
  • 增加編譯的複雜度,必須先處理String literal,才能編譯相依的程式碼

Solution 2: Linker

直接從Linker動手腳

1
$ ld -r -b binary -o binary.o foo.bar  # then link in binary.o
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

extern char _binary_foo_bar_start[];
extern char _binary_foo_bar_end[];

int main(void)
{
printf( "address of start: %p\n", &_binary_foo_bar_start);
printf( "address of end: %p\n", &_binary_foo_bar_end);

for (char* p = _binary_foo_bar_start; p != _binary_foo_bar_end; ++p) {
putchar( *p);
}

return 0;
}

這遇到的問題跟上面差不多

唯一解決的問題是跨平台,複雜度還是無法解決
有沒有更簡單的方法

golang

看看golang的例子,簡單明瞭

1
2
3
4
5
6
import (
_ "embed"
)

//go:embed hello.txt
var s string

embed in C23

終於講到主題了,在C23中可以這麼做

1
2
3
4
5
6
7
8
9
10
11
#include <stddef.h>

void show_icon(const unsigned char *, size_t);

int main (int, char*[]) {
static const unsigned char icon_data[] = {
#embed "black_sheep.ico"
};
show_icon(icon_data, sizeof(icon_data));
return 0;
}

這解決了編譯相依性的問題,不過沒有實質性的改進,C++目前沒有對應的東西
如果你以前的方式跑得好好的,不會因為編譯速度還煩惱的話,沒有必要一定要使用

Reference

在網路上看到computed goto,才知道這不在C/C++標準裡面,只是gcc/clang中的extension,難怪我以前沒聽過
goto就不說了,goto fail太有名了,C/C++都已經建議少用goto,就介紹變種的computed goto

Example

gcc稱computed goto為"label as values",使用label當作pointer,然後用goto jump到執行的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

int main() {
void *labels[] = {&&label1, &&label2, &&label3, &&label4};
int i = 1;

goto *labels[i];

label1:
printf("This is label 1\n");
goto end;

label2:
printf("This is label 2\n");
goto end;

label3:
printf("This is label 3\n");
goto end;

label4:
printf("Invalid label\n");
goto end;

end:
printf("End of program\n");
return 0;
}

Without computed gogo

將前面的範例,用符合標準的方式寫一次,能用的工具通常就是switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main() {
int i = 1;

switch (i) {
case 0:
printf("This is label 1\n");
break;
case 1:
printf("This is label 2\n");
break;
case 2:
printf("This is label 3\n");
break;
default:
printf("Invalid label\n");
break;
}

printf("End of program\n");
return 0;
}

Generated assembly code

先看Switch的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
main: # @main
push rbp
mov rbp, rsp
sub rsp, 48
mov dword ptr [rbp - 4], 0
mov dword ptr [rbp - 8], 1
mov eax, dword ptr [rbp - 8]
test eax, eax
mov dword ptr [rbp - 12], eax # 4-byte Spill
je .LBB0_1
jmp .LBB0_6
.LBB0_6:
mov eax, dword ptr [rbp - 12] # 4-byte Reload
sub eax, 1
mov dword ptr [rbp - 16], eax # 4-byte Spill
je .LBB0_2
jmp .LBB0_7
.LBB0_7:
mov eax, dword ptr [rbp - 12] # 4-byte Reload
sub eax, 2
mov dword ptr [rbp - 20], eax # 4-byte Spill
je .LBB0_3
jmp .LBB0_4
.LBB0_1:
movabs rdi, offset .L.str
mov al, 0
call printf
mov dword ptr [rbp - 24], eax # 4-byte Spill
jmp .LBB0_5
.LBB0_2:
movabs rdi, offset .L.str.1
mov al, 0
call printf
mov dword ptr [rbp - 28], eax # 4-byte Spill
jmp .LBB0_5
.LBB0_3:
movabs rdi, offset .L.str.2
mov al, 0
call printf
mov dword ptr [rbp - 32], eax # 4-byte Spill
jmp .LBB0_5
.LBB0_4:
movabs rdi, offset .L.str.3
mov al, 0
call printf
mov dword ptr [rbp - 36], eax # 4-byte Spill
.LBB0_5:
movabs rdi, offset .L.str.4
mov al, 0
call printf
xor ecx, ecx
mov dword ptr [rbp - 40], eax # 4-byte Spill
mov eax, ecx
add rsp, 48
pop rbp
ret
.L.str:
.asciz "This is label 1\n"

.L.str.1:
.asciz "This is label 2\n"

.L.str.2:
.asciz "This is label 3\n"

.L.str.3:
.asciz "Invalid label\n"

.L.str.4:
.asciz "End of program\n"

LBB0_1.LBB0_4分別對應四個switch case,main的前半段和LBB0_6LBB0_7來決定跳到哪個label去
如果用computed goto的話

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
main: # @main
push rbp
mov rbp, rsp
sub rsp, 96
mov dword ptr [rbp - 4], 0
mov rax, qword ptr [.L__const.main.labels]
mov qword ptr [rbp - 48], rax
mov rax, qword ptr [.L__const.main.labels+8]
mov qword ptr [rbp - 40], rax
mov rax, qword ptr [.L__const.main.labels+16]
mov qword ptr [rbp - 32], rax
mov rax, qword ptr [.L__const.main.labels+24]
mov qword ptr [rbp - 24], rax
mov dword ptr [rbp - 52], 1
movsxd rax, dword ptr [rbp - 52]
mov rax, qword ptr [rbp + 8*rax - 48]
mov qword ptr [rbp - 64], rax # 8-byte Spill
jmp .LBB0_6
.Ltmp1: # Block address taken
movabs rdi, offset .L.str
mov al, 0
call printf
mov dword ptr [rbp - 68], eax # 4-byte Spill
jmp .LBB0_5
.Ltmp2: # Block address taken
movabs rdi, offset .L.str.1
mov al, 0
call printf
mov dword ptr [rbp - 72], eax # 4-byte Spill
jmp .LBB0_5
.Ltmp3: # Block address taken
movabs rdi, offset .L.str.2
mov al, 0
call printf
mov dword ptr [rbp - 76], eax # 4-byte Spill
jmp .LBB0_5
.Ltmp4: # Block address taken
movabs rdi, offset .L.str.3
mov al, 0
call printf
mov dword ptr [rbp - 80], eax # 4-byte Spill
.LBB0_5:
movabs rdi, offset .L.str.4
mov al, 0
call printf
xor ecx, ecx
mov dword ptr [rbp - 84], eax # 4-byte Spill
mov eax, ecx
add rsp, 96
pop rbp
ret
.LBB0_6:
mov rax, qword ptr [rbp - 64] # 8-byte Reload
jmp rax
.L__const.main.labels:
.quad .Ltmp1
.quad .Ltmp2
.quad .Ltmp3
.quad .Ltmp4

.L.str:
.asciz "This is label 1\n"

.L.str.1:
.asciz "This is label 2\n"

.L.str.2:
.asciz "This is label 3\n"

.L.str.3:
.asciz "Invalid label\n"

.L.str.4:
.asciz "End of program\n"

這裡的Ltmp1Ltmp4一樣是對應四個case,而main的前半段就計算該跳到哪個label,在

1
2
3
.LBB0_6:
mov rax, qword ptr [rbp - 64] # 8-byte Reload
jmp rax

直接跳走了,根據benchmark的結果,這樣的作法比swithc快上不少,所以很多emulator和interpreter都採用這種作法

Computed goto and C++

有個很陰險的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

class Test {
public:
Test() { std::cout << "Constructor called\n"; }
~Test() { std::cout << "Destructor called\n"; }
};

int main() {
void* labels[] = {&&label1, &&label2};
int i = 1;

{
Test t;
goto *labels[i];
}

label1:
std::cout << "This is label 1\n";
goto end;

label2:
std::cout << "This is label 2\n";
goto end;

end:
std::cout << "End of program\n";
return 0;
}

在這種情況下,Test的destructor不起作用
如果改成

1
2
3
4
{
Test t;
goto label2;
}

逕行了,不過這通常不是我們要的,可能要依需求修改程式

與其說這是Reflection,這個比較像是General meta programming solution
也就是Boost Hana所推薦的類型運算,將Type當作Value,然後對Type做處理的動作
基本的操作就是這樣

1
2
constexpr std::meta::info value = ^^int;
using Int = typename[:value:];

這裡的info是個opaque type,只有Compiler看得懂,任何的操作要透過Meta function來進行

在開始之前,先寫一個Helper function,好幫助做驗證

Helper function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// start 'expand' definition
namespace __impl {
template<auto... vals>
struct replicator_type {
template<typename F>
constexpr void operator>>(F body) const {
(body.template operator()<vals>(), ...);
}
};

template<auto... vals>
replicator_type<vals...> replicator = {};
}

template<typename R>
consteval auto expand(R range) {
std::vector<std::meta::info> args;
for (auto r : range) {
args.push_back(std::meta::reflect_value(r));
}
return substitute(^^__impl::replicator, args);
}
// end 'expand' definition

template <typename S>
constexpr auto print_layout() -> std::string {
std::string desc = "Print struct layout: " + std::string(identifier_of(^^S)) + "\n";
desc += "members: {\n";
[: expand(nonstatic_data_members_of(^^S)) :] >> [&]<auto e>() mutable {
desc += " " + std::string(identifier_of(e));
desc += " " + std::string(identifier_of(type_of(e)));
desc += "\n";
};
desc += "}";
return desc;
}

測試範例

1
2
3
4
5
6
7
8
9
10
11
12
13
struct X
{
char a;
int b;
double c;
};

int main()
{
auto desc = print_layout<X>();
std::cout << desc << "\n";
return 0;
}

無中生有,define_class

最簡單的例子

1
2
3
4
5
6
7
8
9
struct storage;
static_assert(is_type(define_class(^^storage, {data_member_spec(^^int, {.name="value"})})));

int main()
{
auto desc = print_layout<storage>();
std::cout << desc << "\n";
return 0;
}

印出來的結果

1
2
3
4
Print struct layout: storage
members: {
value int
}

從上面的程式碼,可以看出

  • storage 是 forward declaration,沒有任何定義
  • 真正的定義是在define_class(....) 這裡
  • 必須使用static_assert(is_type(....))在編譯期完成
  • 宣告member field必須使用data_member_spec

    Template class

    如果我們想要產生類似
    1
    2
    3
    4
    template <size_t N>
    struct storage {
    int value[N];
    };
    該怎麼做
    Step1
    雖然無關緊要,不過我想把定義class的部分獨立成一個函數
    1
    2
    3
    4
    5
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(^^storage, {data_member_spec(^^int, {.name="value"})});
    }
    using T = [:define_storage<10>():];
    這樣省下了static_assert(is_type(...)) 的需求
    Step2
    將storage的forward declaration改成這樣
    1
    2
    template <size_t N>
    struct storage;
    Step3
    修改define_storage的實作
    1
    2
    3
    4
    5
    6
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(substitute(^^storage, {^^N}), {
    data_member_spec(^^int, {.name="value"})
    });
    }
    這邊有個substitute函數,第一個參數就是tempalte class,之後的參數就是要填入的直了,印出的結果類似這樣
    1
    2
    3
    4
    Print struct layout: storage
    members: {
    value int
    }
    距離我們要的還差一點
    Step4
    故技重施,定義一個新type
    1
    2
    template <typename T, size_t N>
    using c_array_t = T[N];
    然後再度修改define_storage
    1
    2
    3
    4
    5
    6
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(substitute(^^storage, {^^N}), {
    data_member_spec(substitute(^^c_array_t, {^^int, ^^N}), {.name="value"})
    });
    }
    這下子看到的是
    1
    2
    3
    4
    Print struct layout: storage
    members: {
    value c_array_t
    }
    有沒有辦法將c_array_t變回int[10]
    Step5
    加上dealias就行了,不過應該有更好的方法,暫時還沒想到
    1
    2
    3
    4
    5
    6
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(substitute(^^storage, {^^N}), {
    data_member_spec(dealias(substitute(^^c_array_t, {^^int, ^^N})), {.name="value"})
    });
    }
    不過這方法跟原先的方法比,最大的好處是可程式化,可以創造出更特別的玩法,例如

Full(Partial) Template Specialization

將原先的範例改成這樣

1
2
3
4
5
6
7
8
9
template <size_t N>
struct storage {
int value[N];
};
template <>
struct storage<5> {
char value[5];
int value2;
};

我們一樣可以用define_storage來做

1
2
3
4
5
6
7
8
9
10
11
template <size_t N>
consteval auto define_storage() -> std::meta::info {
std::vector<std::meta::info> members;
if constexpr (N == 5) {
members.push_back(data_member_spec(dealias(substitute(^^c_array_t, {^^char, ^^N})), {.name="value"}));
members.push_back(data_member_spec(^^int, {.name="value2"}));
} else {
members.push_back(data_member_spec(dealias(substitute(^^c_array_t, {^^int, ^^N})), {.name="value"}));
}
return define_class(substitute(^^storage, {^^N}), members);
}

而Partial Template Specialization 可以用同樣的方式處理

1
2
3
4
5
6
7
8
9
10
11
template <size_t N1, size_t N2>
struct storage {
int value[N1];
int value2[N2];
};

template <size_t N1>
struct storage<N1, 5> {
int value1[N1];
char value2[5];
};

之後可以這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
template <size_t N1, size_t N2>
struct storage;
template <size_t N1, size_t N2>
consteval auto define_storage() -> std::meta::info {
std::vector<std::meta::info> members;
members.push_back(data_member_spec(dealias(substitute(^^c_array_t, {^^int, ^^N1})), {.name="value1"}));
if constexpr (N2 == 5) {
members.push_back(data_member_spec(dealias(substitute(^^c_array_t, {^^char, ^^N2})), {.name="value2"}));
} else {
members.push_back(data_member_spec(dealias(substitute(^^c_array_t, {^^int, ^^N2})), {.name="value2"}));
}
return define_class(substitute(^^storage, {^^N1, ^^N2}), members);
}

Implement Pick/Omit in TypeScript

Typescript裡面有一個Pick Utility

1
2
3
4
5
6
type Person = {
firstName: string;
lastName: string;
age: number;
};
type PersonName = Pick<Person, 'firstName' | 'lastName'>;

這裡的PersonName就等同於

1
2
3
4
type PersonName = {
firstName: string;
lastName: string;
};

C++的等價版本應該是這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename From, typename To>
consteval auto Pick(std::initializer_list<std::string_view> keeps) -> std::meta::info {
std::vector<std::meta::info> members;
[: expand(nonstatic_data_members_of(^^From)) :] >> [&]<auto e>() mutable {
auto it = std::ranges::find(keeps, identifier_of(e));
if (it != keeps.end()) {
members.push_back(data_member_spec(type_of(e), {.name=identifier_of(e)}));
}
};
return define_class(^^To, members);
}

struct Person {
std::string firstName;
std::string lastName;
int age;
};

struct PersonName;
static_assert(is_type(Pick<Person, PersonName>({ "firstName", "lastName" })));

應該可以更好,不過目前想不到怎麼做
Omit只是Pick的反操作,這邊就不寫了

Revisit Boost MP11

以下是MP11的一個範例

1
2
3
using L = std::tuple<void, int, float>;
using R = mp_transform<std::add_pointer_t, L>;
static_assert(std::is_same_v<R, std::tuple<void*, int*, float*>>);

可以用Meta Programming重寫

1
2
3
4
5
6
7
8
9
10
11
template < typename T>
consteval auto transform(std::meta::info (&F)(std::meta::info)) -> std::meta::info {
std::vector<std::meta::info> new_members;
for (auto member : template_arguments_of(^^T)) {
new_members.push_back(F(member));
}
return substitute(template_of(^^T), new_members);
}
using L = std::tuple<void, int, float>;
using R = [:transform<L>(std::meta::type_add_pointer):];
static_assert(std::is_same_v<R, std::tuple<void*, int*, float*>>);

省去了很多以往的Template Magic

Conclusion

Reflection如果如期進入C++26,會是極大的變化
只希望一切順利

Reference

C23有一項特性

Structure, union and enumeration types may be defined more than once in the same scope with the same contents and the same tag; if such types are defined with the same contents and the same tag in different scopes, the types are compatible.

以下的程式碼在C23是合法的

1
2
3
4
5
6
7
struct A {
    int a;
};

struct A {
    int a;
};

而在C23之前會被歸類成 redefinition of 'struct A'
有了這個,在C語言寫類Generic 會比較方便
例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#define Result_t(T, E) struct Result_##T##_##E { bool is_ok; union { T value; E error; }; }

#define Ok(T, E) (struct Result_##T##_##E){ .is_ok = true, .value = (T) _OK_IMPL
#define _OK_IMPL(...) __VA_ARGS__ }

#define Err(T, E) (struct Result_##T##_##E){ .is_ok = false, .error = (E) _ERR_IMPL
#define _ERR_IMPL(...) __VA_ARGS__ }

typedef const char *ErrorMessage_t;

Result_t(int, ErrorMessage_t) my_func(int i)
{
if (i == 42) return Ok(int, ErrorMessage_t)(100);
else return Err(int, ErrorMessage_t)("Cannot do the thing");
}

int main()
{
Result_t(int, ErrorMessage_t) x = my_func(42);

if (x.is_ok) {
printf("%d\n", x.value);
} else {
printf("%s\n", x.error);
}
}

目前只有GCC 14.1支持

不過事情總不可能永遠事事順利,總是有會碰到支援舊的Compiler這種事
這邊看到不錯的解決方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#if __STDC_VERSION__ >= 202311L
#define Neat_SString(cap) \
struct SString_##cap \
{ \
unsigned int len; \
unsigned char chars[ cap + 1 ]; /* + 1 for the nul */ \
}

#define NEAT_DECL_SSTRING(cap)
#else
#define Neat_SString(cap) \
struct SString_##cap

#define NEAT_DECL_SSTRING(cap) \
struct SString_##cap \
{ \
unsigned int len; \
unsigned char chars[ cap + 1 ]; /* + 1 for the nul */ \
}
#endif

Reference