找不到適合的工作,只好多方嘗試,看有沒有寫書的才華
寫了一本有關C/C++生態系的書
C/C++編譯器與它的快樂夥伴
Revisit C++20 Module and CMake
由於打算寫本電子書,所以重新審視了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 | target_sources(MathFunctions |
這邊有兩個FILE_SET
- primary_interface:也就是我們要對外提供的Primary module interface unit
- implementaion_units:內部的partion unit,不對外輸出
所以在安裝的時候,只會將MathFunctions.cppm複製到安裝的目錄下Case2: Multiple Primary Module Interface Units
接著我們稍微修改MathFunctions.cppm的內容我們也將detail的內容也輸出了,因此我們需要做以下的修改1
2
3
4
5
6
7
8
9module;
export module Math;
export import :detail;
export namespace mathfunctions
{
double sqrt(double);
} - detail module和namespace需要標記成
export修改我們的CMakeLists.txt的部分1
2
3
4
5
6
7
8
9
10module;
#include <math.h>
export module Math:detail;
export namespace mathfunctions::detail {
double sqrt(double x) {
return ::sqrt(x);
}
}現在我們有了兩個Primary Module Interface Units,在安裝的時候也要同時複製兩個檔案1
2
3
4
5
6
7
8
9
10target_sources(MathFunctions
PUBLIC
FILE_SET primary_interface
TYPE CXX_MODULES
FILES
MathFunctions.cppm
src/mysqrt.cppm
PRIVATE
src/MathFunctions.cxx
)Math.detail和Math:detail的情況類似,所以就不說了
接著來研究Mitgrate的部分,這是參考clang Transitioning to modules的部分
Case3: Mitgrate legacy to module (Part1)
看一下transform_1的目錄
這邊主要的差別在於CMakeLists.txt
1 | target_sources(MathFunctions |
既保留原有的leagcy code,更新增了一個Primary Module Interface Units
而MathFunctions.cppm的內容則是
1 | module; |
將Global Module Fragment中的內容導出到Module中
這種方法不會破壞原有leagcy code,殺傷力最小
Case4: Mitgrate legacy to module (Part2)
看一下transform_21的目錄,CMakeLists.txt跟上面一樣不變
改變的是MathFunctions.cppm和MathFunctions.h
此時的MathFunctions.cppm長這樣
1 | module; |
而MathFunctions.h的內容則是
1 | #pragma once |
由於只有在Module狀態下,IN_MODULE_INTERFACE才會發揮作用,因此leagcy code的情況下會維持不變
這個方法雖然比上面麻煩,不過可以順利遷移到下一個階段
Case5: Mitgrate legacy to module (Part3)
所有方案中最麻煩的一種
主要思想是在implemtation unit當中切開legacy和module的實作,強迫Consumer只能使用其中一種,例如原先的Header可能要加上export
1 | #pragma once |
以及Implementation的部分也要隔開
1 | #ifndef IN_MODULE_INTERFACE |
在這裡我選擇對CMakeLists.txt動手腳
1 | if (ENABLE_MODULE_BUILD) |
當我們指定ENABLE_MODULE_BUILD的時候,會自動處理細節的部分
不過這邊也遇到了clang文件中的問題
Minor issue
由於我們之前的mysqrt.h是經由src/MathFunctions.cxx所include的,改成Module之後,這個相依性被切斷了
因此我們需要在MathFunctions.cppm強迫加入
1 | module; |
這樣沒有問題,不過
- 原來的
mysqrt.h不需要公開,現在變成強迫要公開了 - 更好的方法是直接使用Module Partition Unit,也就是要改寫
Simulation for compile-time loop
問題說來很簡單,結果我花了好久才搞懂,字字血淚
我想要的只有這樣的效果
1 | template <std::size_t i> |
結果這樣是無法編譯的,只好找其他方法繞過
Solution 1: Template specialization
一樣是從別人的Code上學來的
1 | template <std::size_t i> |
在這邊
_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
5template <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的參數,很可惜這不是
於是先擱置在一旁,不過這個方案接近我最後的方案,還是感激一下AICustom apply function
這個是GPT4-o提供的將fold expression隱藏於實作中,雖然不錯,不過這方法也是有其缺點1
2
3
4
5
6
7
8
9
10template <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>{});
}
};
無法表示這樣的Pseudo code萬能的CharGPT也給了一個思路,拓展apply_impl1
2
3
4
5
6
7
8
9
10
11
12template <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;
}看起來可行,不過也是有自己的問題1
2
3
4
5
6
7
8
9
10
11
12template <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 like的index_sequqence就好了這樣就沒啥問題了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20template <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,不過不知會部會進標準
deduce this for API design
API Design
看了lexy的程式碼之後,對其API Design很有興趣
其中有一個設計
1 | template <typename Fn> |
這API的重點
- 我們只希望有一個Root
parse_state - 只能透過 Util function
map,可以創建另一個placeholder_base的subclass
lexy採用的是Partial template specialization方案來應對這個設計,不過這方案有個缺點,大量重複的程式碼
以上面的例子來看,operator()的內容幾乎一樣,如果功能更多的話,可能會有更多類似但卻不相同的函數出現,可能增加維護的難度deduce this
deduce this的教學文章很多了,拾人牙慧也沒什麼意思
雖然不確定是否是最佳解,不過可以試試看設計這樣的API這邊的map跟lexy上面的map差不多1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct 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{}; - 只能透過
parse_state來使用util functionmap
而operator()就能共用大部分的邏輯了
不過這方法也是有缺點,對邊義氣版本的要求很高 - gcc目前要14才有支援
- clang目前也是到18才支持
- MSVC至少要17.2
CRTP
在寫完後不久,才想起CRTP也可以解決這問題
1 | template <typename Derived = void> |
不過比起deduce this,可以要麻煩一點
Reference
embed in C23
其實這也不是什麼新玩意,不過由於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 | #include <stdio.h> |
這遇到的問題跟上面差不多
- Windows 不支持這種做法
- 一樣多了複雜性
Solution 3: CMake script
雖然原理可能不同,不過最後的結果大同小異 - Embedding Binary Data in Executable with CMake
- battery-embed
- CMake embed
唯一解決的問題是跨平台,複雜度還是無法解決
有沒有更簡單的方法
golang
看看golang的例子,簡單明瞭
1 | import ( |
embed in C23
終於講到主題了,在C23中可以這麼做
1 | #include <stddef.h> |
這解決了編譯相依性的問題,不過沒有實質性的改進,C++目前沒有對應的東西
如果你以前的方式跑得好好的,不會因為編譯速度還煩惱的話,沒有必要一定要使用
Reference
goto and computed goto
在網路上看到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 | #include <stdio.h> |
Without computed gogo
將前面的範例,用符合標準的方式寫一次,能用的工具通常就是switch
1 | #include <stdio.h> |
Generated assembly code
先看Switch的版本
1 | main: # @main |
LBB0_1到.LBB0_4分別對應四個switch case,main的前半段和LBB0_6和LBB0_7來決定跳到哪個label去
如果用computed goto的話
1 | main: # @main |
這裡的Ltmp1到Ltmp4一樣是對應四個case,而main的前半段就計算該跳到哪個label,在
1 | .LBB0_6: |
直接跳走了,根據benchmark的結果,這樣的作法比swithc快上不少,所以很多emulator和interpreter都採用這種作法
Computed goto and C++
有個很陰險的地方
1 | #include <iostream> |
在這種情況下,Test的destructor不起作用
如果改成
1 | { |
逕行了,不過這通常不是我們要的,可能要依需求修改程式
Meta Programming in C++26
與其說這是Reflection,這個比較像是General meta programming solution
也就是Boost Hana所推薦的類型運算,將Type當作Value,然後對Type做處理的動作
基本的操作就是這樣
1 | constexpr std::meta::info value = ^^int; |
這裡的info是個opaque type,只有Compiler看得懂,任何的操作要透過Meta function來進行
在開始之前,先寫一個Helper function,好幫助做驗證
Helper function
1 | // start 'expand' definition |
測試範例
1 | struct X |
無中生有,define_class
最簡單的例子
1 | struct storage; |
印出來的結果
1 | Print struct layout: storage |
從上面的程式碼,可以看出
- storage 是 forward declaration,沒有任何定義
- 真正的定義是在
define_class(....)這裡 - 必須使用
static_assert(is_type(....))在編譯期完成 - 宣告member field必須使用
data_member_specTemplate class
如果我們想要產生類似該怎麼做1
2
3
4template <size_t N>
struct storage {
int value[N];
};Step1
雖然無關緊要,不過我想把定義class的部分獨立成一個函數這樣省下了1
2
3
4
5template <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
2template <size_t N>
struct storage;Step3
修改define_storage的實作這邊有個1
2
3
4
5
6template <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
4Print struct layout: storage
members: {
value int
}Step4
故技重施,定義一個新type然後再度修改1
2template <typename T, size_t N>
using c_array_t = T[N];define_storage這下子看到的是1
2
3
4
5
6template <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
4Print struct layout: storage
members: {
value c_array_t
}c_array_t變回int[10]Step5
加上dealias就行了,不過應該有更好的方法,暫時還沒想到不過這方法跟原先的方法比,最大的好處是可程式化,可以創造出更特別的玩法,例如1
2
3
4
5
6template <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 | template <size_t N> |
我們一樣可以用define_storage來做
1 | template <size_t N> |
而Partial Template Specialization 可以用同樣的方式處理
1 | template <size_t N1, size_t N2> |
之後可以這樣寫
1 | template <size_t N1, size_t N2> |
Implement Pick/Omit in TypeScript
Typescript裡面有一個Pick Utility
1 | type Person = { |
這裡的PersonName就等同於
1 | type PersonName = { |
C++的等價版本應該是這樣
1 | template <typename From, typename To> |
應該可以更好,不過目前想不到怎麼做
Omit只是Pick的反操作,這邊就不寫了
Revisit Boost MP11
以下是MP11的一個範例
1 | using L = std::tuple<void, int, float>; |
可以用Meta Programming重寫
1 | template < typename T> |
省去了很多以往的Template Magic
Conclusion
Reflection如果如期進入C++26,會是極大的變化
只希望一切順利
Reference
Redefinition type in the same scope for C23
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 | struct A { |
而在C23之前會被歸類成 redefinition of 'struct A'
有了這個,在C語言寫類Generic 會比較方便
例如
1 | #include <stdio.h> |
目前只有GCC 14.1支持
不過事情總不可能永遠事事順利,總是有會碰到支援舊的Compiler這種事
這邊看到不錯的解決方法
1 | #if __STDC_VERSION__ >= 202311L |
Reference
MSVC and __VA_ARGS__
今天才知道這個Bug
1 | #define F(x, ...) X = x and VA_ARGS = __VA_ARGS__ |
gcc和clang的結果都是
1 | X = 1 and VA_ARGS = 2, 3 |
不過MSVC的結果是
1 | X = 1 and VA_ARGS = 2, 3 |
把以前的bug當feature了
修正方法有兩個
一個是在編譯的時候加上/Zc:preprocessor,不過CMake project預設就開了
另一個是加上另外一層Macro
1 | #define EXPAND(x) x |
Introduction to Sender and Receiver
std::execution的部分繞不開Sender/Receiver,經過多次失敗之後終於寫出一個能跑的,紀錄一下
Simplest Receiver
由於Receiver的範例比Sender簡單,所以從Receiver開始,而Sender先用Just代替
1 | #include <stdexec/execution.hpp> |
不過光是這樣一點用都沒有
至少要有有一個Callback function
1 | struct Recv { |
這樣才能跟Sender做結合
1 | auto o1 = stdexec::connect(stdexec::just(1), Recv()); |
至於Callback參數的形式,需要從Sender那邊定義,之後會寫一個簡單的Sender
Simplest Sender
1 | struct Send { |
跟Rece類似,這邊要有一個sender_concept
不過一樣沒什麼用,最小的實現至少是這樣子
1 | struct Send { |
使用方式跟上面差不多
1 | auto o2 = stdexec::connect(Send{}, Recv()); |
先不看op的部分,在Send有兩個部分
1 | using completion_signatures = stdexec::completion_signatures<stdexec::set_value_t(int)>; |
這個定義皆在後面的Receiver該接受什麼類型的參數
對照Recv
1 | struct Recv { |
兩個需要成對,不然connect的部分會出錯
在connect的階段,演算法會呼叫
1 | friend op<R> tag_invoke(stdexec::connect_t, Send, R r) { |
tag_invoke的地方不細說,由於我們不知道真正的Receiver類型是什麼,所以需要一個template版本的op
這邊也只有將Sender和Receiver連接起來,還沒開始執行
執行的部分在
1 | stdexec::start(o2); |
演算法這時候就會呼叫
1 | template <typename R> |
將42送到Receiver
Reference
– [What are Senders Good For, Anyway?]What are Senders Good For, Anyway? – Eric Niebler
– 浅谈The C++ Executors
– c++ execution 与 coroutine (一) : CPO与tag_invoke
– c++ execution 与 coroutine (二) : execution概述
– c++ execution 与 coroutine (三):最简单的receiver与最简单的sender