0%

由於打算寫本電子書,所以重新審視了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

今天才知道這個Bug

1
2
3
4
#define F(x, ...) X = x and VA_ARGS = __VA_ARGS__
#define G(...) F(__VA_ARGS__)
F(1, 2, 3)
G(1, 2, 3)

gcc和clang的結果都是

1
2
X = 1 and VA_ARGS = 2, 3
X = 1 and VA_ARGS = 2, 3

不過MSVC的結果是

1
2
X = 1 and VA_ARGS = 2, 3
X = 1, 2, 3 and VA_ARGS =

把以前的bug當feature了
修正方法有兩個
一個是在編譯的時候加上/Zc:preprocessor,不過CMake project預設就開了
另一個是加上另外一層Macro

1
2
3
#define EXPAND(x) x
#define F(x, ...) X = x and VA_ARGS = __VA_ARGS__
#define G(...) EXPAND(F(__VA_ARGS__))

std::execution的部分繞不開Sender/Receiver,經過多次失敗之後終於寫出一個能跑的,紀錄一下

Simplest Receiver

由於Receiver的範例比Sender簡單,所以從Receiver開始,而Sender先用Just代替

1
2
3
4
5
#include <stdexec/execution.hpp>
struct recv {
using receiver_concept = stdexec::receiver_t;
};
static_assert(stdexec::receiver<recv>);

不過光是這樣一點用都沒有
至少要有有一個Callback function

1
2
3
4
5
6
7
struct Recv {
using receiver_concept = stdexec::receiver_t;

friend void tag_invoke(set_value_t, Recv&&, int v) noexcept {
std::cout << "get value: " << v << "\n";
}
};

這樣才能跟Sender做結合

1
2
auto o1 = stdexec::connect(stdexec::just(1), Recv());
stdexec::start(o1);

至於Callback參數的形式,需要從Sender那邊定義,之後會寫一個簡單的Sender

Simplest Sender

1
2
3
4
struct Send {
using sender_concept = stdexec::sender_t;
};
static_assert(stdexec::sender<Send>);

Rece類似,這邊要有一個sender_concept
不過一樣沒什麼用,最小的實現至少是這樣子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Send {
template <typename R>
struct op {
R r_;
friend void tag_invoke(stdexec::start_t, op& self) noexcept {
stdexec::set_value(std::move(self.r_), 42);
}
};

using sender_concept = stdexec::sender_t;
using completion_signatures = stdexec::completion_signatures<stdexec::set_value_t(int)>;
template <class R>
friend op<R> tag_invoke(stdexec::connect_t, Send, R r) {
return { r };
}
};

使用方式跟上面差不多

1
2
auto o2 = stdexec::connect(Send{}, Recv());
stdexec::start(o2);

先不看op的部分,在Send有兩個部分

1
using completion_signatures = stdexec::completion_signatures<stdexec::set_value_t(int)>;

這個定義皆在後面的Receiver該接受什麼類型的參數
對照Recv

1
2
3
struct Recv {
friend void tag_invoke(set_value_t, Recv&&, int v) noexcept {}
};

兩個需要成對,不然connect的部分會出錯
connect的階段,演算法會呼叫

1
2
3
friend op<R> tag_invoke(stdexec::connect_t, Send, R r) {
return { r };
}

tag_invoke的地方不細說,由於我們不知道真正的Receiver類型是什麼,所以需要一個template版本的op
這邊也只有將SenderReceiver連接起來,還沒開始執行
執行的部分在

1
stdexec::start(o2);

演算法這時候就會呼叫

1
2
3
4
5
6
7
template <typename R>
struct op {
R r_;
friend void tag_invoke(stdexec::start_t, op& self) noexcept {
stdexec::set_value(std::move(self.r_), 42);
}
};

將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

趁出去玩之前發一下文章,在C++ Reflection還沒正式定案之前,總有自救會想盡辦法來解決這些問題

Pre C++20

在C++20之前,最簡單的方法就是用Macro了

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
#include <iostream>

#define PRINT_FIELD_NAME(struct_type, field) \
std::cout << #field << std::endl;

template <typename T>
void printFieldNames(const T& obj) {
// Do nothing in this template; specialize it for each struct
}

#define SPECIALIZE_PRINT_FIELD_NAMES(struct_type, ...) \
template <> \
void printFieldNames<struct_type>(const struct_type& obj) { \
__VA_ARGS__ \
}

// Define your struct
struct MyStruct {
int field1;
double field2;
char field3;
};

// Specialize the printFieldNames template for your struct
SPECIALIZE_PRINT_FIELD_NAMES(MyStruct,
PRINT_FIELD_NAME(MyStruct, field1)
PRINT_FIELD_NAME(MyStruct, field2)
PRINT_FIELD_NAME(MyStruct, field3)
)

int main() {
MyStruct myObj;
printFieldNames(myObj);

return 0;
}

Macro的方案都差不多,不過問題是出在Macro,由於在Preprocessor階段,得不到C++2的語意,所以會遇上以下問題

  • 難維護
  • 新增新功能困難

接著就到了C++20時期了

Core knowledge

在這之前要先介紹一個核心知識,沒有這個都辦不到

1
2
3
4
5
6
7
8
9
10
11
12
// C++17
template<typename T>
void test() {
std::cout << __PRETTY_FUNCTION__ << '\n';
}

// C++20
#include <source_location>
template <typename T>
void test() {
std::cout << std::source_location::current().function_name() << "\n";
}

__PRETTY_FUNCTION__是gcc/clang特有的,Visual C++有等價的__FUNCSIG__,不過在C++20之後,用std::source_location::current().function_name()就好,接下來的問題就變成了,如何將struct的information成為funcion name的一部分`

Non-type Template Parameters in C++20

在C++20當中,NTTP能居受的種類更多了,由於這樣,很難推斷出類型,所以template <auto> 發揮出功用了

1
2
3
4
5
6
7
8
9
10
11
12
13
template <auto V>
void test() {
std::cout << std::source_location::current().function_name() << "\n";
}

struct obj {
int field;
};

int main() {
test<&obj::field>();
}
`

輸出結果

1
void test() [with auto V = &obj::field]

到目前為止,這段程式碼還不是太有用,因為印出了太多不需要的東西,所以要對輸出Function Name做處理

Process Function Name

用constexpr std::string_view做處理

1
2
3
4
5
6
7
8
9
10
11
template <auto V>
constexpr std::string_view get_name()
{
std::string_view funcname = std::source_location::current().function_name();
auto pos = funcname.find("=");
funcname = funcname.substr(pos);
pos = funcname.find("::");
funcname = funcname.substr(pos + 2);
pos = funcname.find(";");
return funcname.substr(0, pos);
}

MSVC和gcc/clang的處理方式不同,要個別處理

Reference