0%

其實這也不是什麼新玩意,不過由於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

出去玩了一趟,好久沒寫一些東西,不然都要乾涸了
這觀念也很簡單,假設我們有類似這樣的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
void foo()
{
if constexpr (std::is_same_v<T, int>)
{
// handle int case
}
else if constexpr (std::is_same_v<T, float>)
{
// handle float case
}
// ... other cases
else
{
static_assert(false, "T not supported");
}
}

這段程式在C++20是編譯不過,可是C++23放鬆了限制,允許這種寫法
不過根據神人的解法,在C++20可以模擬這種動作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <class... T>
constexpr bool always_false = false;

template <typename T>
void foo()
{
if constexpr (std::is_same_v<T, int>)
{
// handle int case
}
else if constexpr (std::is_same_v<T, float>)
{
// handle float case
}
// ... other cases
else
{
static_assert(always_false<T>, "T not supported");
}
}

雖然繞了一點,但是能用

Reference

What is type punning

Type Punning是指用不同類型的Pointer,指向同一塊Memory address的行為,這是Undefined beahvior,可能會造成未知的錯誤.
例如

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
float f = 3.14;
int* pi = (int*)&f;
*pi = 42;
std::cout << "f = " << f << std::endl;
return 0;
}

Type punning違反了Strict aliasing rule

Example

寫網路程式的時候常常會遇到這種情形,分配一塊記憶體,然後Cast成另外一種Type的Pointer填值

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
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));

// Alias that buffer through message
Msg* msg = (Msg*)(buff);

// Send a bunch of messages
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}

Solution

C Solution

union

C語言的話可以使用union

1
2
3
4
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
char*

或是使用(unisnged / signed) char *取代上面的int*
可以認為j從char*轉匯成type *是合法的,反之不成立

memcpy
1
2
3
int x = 42; 
float y;
std::memcpy(&y, &x, sizeof(x));

這樣是合法的,不過缺點就是要多一次拷貝

C++ Solution

bit_cast

C++20引進的新東西,不過實作也就只是上面的memcpy包裝

1
2
3
4
5
6
7
template <class To, class From>
bit_cast(const From& src) noexcept
{
To dst;
std::memcpy(&dst, &src, sizeof(To));
return dst;
}
std::start_lifetime_as

C++23引進的新觀念,類似於reinterpret_cast,不過沒有undefined behaviro的副作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ProtocolHeader {
unsigned char version;
unsigned char msg_type;
unsigned char chunks_count;
};

void ReceiveData(std::span<std::byte> data_from_net) {
if (data_from_net.size() < sizeof(ProtocolHeader)) throw SomeException();
const auto* header = std::start_lifetime_as<ProtocolHeader>(
data_from_net.data()
);
switch (header->type) {>
// ...
}
}

Reference

一開始看到GAT也不知道在幹嘛,是看到Could someone explain the GATs like I was 5?才有感覺]
最簡單的範例,現在有一個struct

1
struct Foo { bar: Rc<String>, }

假設你要同時支援 Rc和’Arc的版本
該怎麼做

Naive solution

1
2
struct FooRc { bar: Rc<String>, }
struct FooArc { bar: Arc<String>, }

不過這當然沒什麼好說的

Macro solution

理論上辦得到,不過沒什麼優點

GAT Solution

我希望能寫成這樣

1
struct Foo<P: Pointer> { bar: P<String>, }

這樣是編譯不會過的,有了GAT之後,可以寫成這樣

1
2
3
4
5
6
7
trait PointerFamily { type Pointer<T>; }
struct RcFamily; // Just a marker type; could also use e.g. an empty enum
struct ArcFamily; // Just a marker type; could also use e.g. an empty enum
impl PointerFamily for RcFamily { type Pointer<T> = Rc<T>; }
impl PointerFamily for ArcFamily { type Pointer<T> = Arc<T>; }

struct Foo<P: PointerFamily> { bar: P::Pointer<String>, }

C++ Solution

不過用C++對我來說反而更好理解,就是用nested template來做
首先是等價的版本

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
template <typename T>
struct Rc {};
template <typename T>
struct Arc {};

struct RcFamily {
    template <typename T>
    using type = Rc<T>;
};

struct ArcFamily {
    template <typename T>
    using type = Arc<T>;
};

template <typename T>
struct PointerFamily {
    template <typename U>
    using type = T::template type<U>;
};

template <typename T>
struct Foo {
    typename PointerFamily<T>::template type<std::string> bar;
};

不過對於這問題,還有更簡單的方法
用template template parameter即可

1
2
3
4
5
6
7
8
9
template <typename T>
struct Rc {};
template <typename T>
struct Arc {};

template <template <typename> class T>
struct Foo {
    T<std::string> bar;
};

More complicated example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
trait Mappable {
type Item;
type Result<U>;
fn map<U, P: FnMut(Self::Item) -> U>(self, f: P) -> Self::Result<U>;
}

impl<T> Mappable for Option<T> {
type Item = T;
type Result<U> = Option<U>;
fn map<U, P: FnMut(Self::Item) -> U>(self, f: P) -> Option<U> {
self.map(f)
}
}

impl<T, E> Mappable for Result<T, E> {
type Item = T;
type Result<U> = Result<U, E>;
fn map<U, P: FnMut(Self::Item) -> U>(self, f: P) -> Result<U, E> {
self.map(f)
}
}

等價的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
template <class T>
struct Option {
    // Option implementation...
    // The "GAT":

    template <class U>
    using MapResult = Option<U>;
    template <class U, class F>

    Option<U> map(F f) {
        // Apply f to the contents of `this`
    }
};

template <class T>
concept Mappable = requires {
    typename T::template MapResult<int>;
};

template <Mappable T>
typename T::template MapResult<int> zero_to_42(T t) {
    return t.template map<int>([](int x) {
        return x == 0 ? 42 : 0 ;
    });
}