0%

namespace

由於繼承自C語言,所以會遇到像這樣的問題

1
2
3
4
5
6
// my_std.h
void foo(int);
void bar(void);
// other_lib.h
int foo(void);
int baz(int, int);

來自於不同的Library,且提供不同的實作,在使用上會出現一些問題
而C語言時代的解法就是對Function Name加料

1
2
3
4
5
6
// my_std.h
void my_std_foo(int);
void my_std_bar(void);
// other_lib.h
int other_lib_foo(void);
int other_lib_baz(int, int);

而C++做的事情差不多,用namespace隔開

1
2
3
4
5
6
7
8
9
10
// my_std.h
namespace my_std {
void foo(int);
void bar(void);
}
// other_lib.h
namespace other_lib {
int foo(void);
int baz(int, int);
}

ADL

全名是Argument-Dependent Lookup
只要有一個參數在函數的命名空間內,在使用的時候就不用加namespace prefix
在ADL只關心函數,不包含Function Object,這點在之後會用到

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace A
{
struct Empty {};
void foo(int) {}
void bar(Empty, int) {}
}

void func()
{
A::foo(2);
bar(A::Empty{}, 1);
std::cout << 1; // operator<< (std::cout, 1) Due to ADL
}

如果沒有ADL,最後那行只能這樣寫了

1
std::operator<<(std::cout, 1);

應該沒人會喜歡

Example for std::swap

這是拓展問題的最好範例

1
2
3
4
5
6
7
8
namespace std {
template<typename T>
void swap(T& a, T& b) {
T temp(a);
a = b;
b = temp;
}
}

如果我們要對自己的class做swap動作時,該怎麼做

1
2
3
4
5
6
namespace My {
class A {
public:
void swap(A&) {}
};
}

直覺的寫法可以這樣做

1
2
3
4
5
6
```cpp
namespace std
{
template<>
void swap<::My::A>(::My::A& a, ::My::A& b) {a.swap(b);}
}

這樣寫是Undefined Beahvior
而另外一種做法是

1
2
template<>
void std::swap<My::A>(My::A& a, My::A& b) { a.swap(b); }

不過如果是My::A<T>的話就不管用了
而比較常用的手法,就是利用ADL

1
2
3
4
5
6
7
8
9
10
11
12
void fun(...); // 1 
namespace My {
struct A{};
void fun(const A&); // 2
}
namespace Code {
void fun(int); // 3
void use() {
::My::A a;
fun(a); // HERE
}
}

呼叫的foo(a)時,會考慮2和3,1是因為在Code的namespace已經找到一個fun了,部會在往上層的scope去尋找
利用ADL two-step的手法來拓展我們的std::swap

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
#include <utility>
namespace My
{
struct A
{
friend void swap(A& a, A& b) { a.swap(b); }
};
template<typename T>
struct B
{
friend void swap(B& a, B& b) { a.swap(b); }
};
}
namespace Code
{
void use()
{
using std::swap;
::My::A a1, a2;
swap(a1, a2); // HERE #1
::My::B<int> b1, b2;
swap(b1, b2); // HERE #2
int i1, i2;
swap(i1, i2); // NOPE
}
}

在這個範例當中,呼叫swap的時候沒加上namespace,而讓std::swap注入當今的Scope下,如果可以透過ADL找到對應的函數,則用特化版的函數,不然就用原先的std::swap做預設值

Drawback on ADL two-step

最大的問題在於

1
2
using std::swap;
swap(a1, a2);

可能一不小心就寫成

1
std::swap(a1, a2);

不會報錯,頂多是效能差
另外一個比較大的問題是這個

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace __my_std_impl
{
template<typename T>
auto __distance_impl(T first, T last) {/* ... */}
template<typename T>
auto distance(T first, T last) {return __distance_impl(first, last);}
}
struct incomplete;
template<typename T> struct box {T value;};
void use()
{
incomplete* i = nullptr; // fine
__my_std_impl::distance(i, i); // fine
box<incomplete>* b = nullptr; // fine
__my_std_impl::distance(b, b); // !!!
}

__my_std_impl::distance(b, b)的地方會報錯
原因在於__distance_impl階段會進行ADL動作,在box的定義上尋找是否有__distance_impl的函數,因找到incomplete value,故報錯
一種可能的解法就是加上namespace

1
2
template<typename T>
auto distance(T first, T last) {return __my_std_impl::__distance_impl(first, last);}

Customization Point Object

兩階段ADL的最大問題就是容易誤用
因此叫Standlard library來幫你做這件事
其中最簡單的CPO就長這樣

1
2
3
4
5
6
namespace std::ranges {
inline constexpr swap = [](auto& a, auto& b) {
using std::swap;
swap(a, b);
};
}

這裡的swap是個constexpr object,而不是個function,不過他是一個functor,因此可以適用於所有std::swap的環境下
CPO還有一個優勢,它是一個object,所以它能夠這樣用

1
some_ranges | views::transform(ranges::begin)

1
some_ranges | views::transform(std::begin)

這樣用不合法,因為它是個template function

Niebloids

Niebloids是要解決另外一個問題,去除掉不想要的ADL candicate
禁用的方法就是讓它成為CPO
以下是StackOverflow的範例

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
#include <iostream>
#include <type_traits>
namespace mystd
{
class B{};
class A{};
template<typename T>
void swap(T &a, T &b)
{
std::cout << "mystd::swap\n";
}
}

namespace sx
{
namespace impl {
//our functor, the niebloid
struct __swap {
template<typename R, typename = std::enable_if_t< std::is_same<R, mystd::A>::value > >
void operator()(R &a, R &b) const
{
std::cout << "in sx::swap()\n";
// swap(a, b);
}
};
}
inline constexpr impl::__swap swap{};
}

int main()
{
mystd::B a, b;
swap(a, b); // calls mystd::swap()

using namespace sx;
mystd::A c, d;
swap(c, d); //No ADL!, calls sx::swap!

return 0;
}

如果找到的是function object,則不會使用ADL

tag_invoke

根據libunifex裡面的描述,一樣是透過ADL,要解決以下兩個問題

  1. Each one internally dispatches via ADL to a free function of the same name, which has the effect of globally reserving that identifier (within some constraints). Two independent libraries that pick the same name for an ADL customization point still risk collision.
  2. There is occasionally a need to write wrapper types that ought to be transparent to customization. (Type-erasing wrappers are one such example.) With C++20’s CPOs, there is no way to generically forward customizations through the transparent wrap
    比較大的問題是第一點,由於透過ADL尋找函數,所以每個namespace下都需要將函數名稱當作保留字
1
2
3
4
5
6
7
8
9
10
11
12
namespace std::range {
inline constexpr swap = [](auto& a, auto& b) {
using std::swap;
swap(a, b);
};
}
namespace A {
void swap(...);
}
nameapce B {
void swap(....);
}

也就是你用了swap當CPO之後,其他地方都要保留swap當作保留字不能使用,tag_invoke就是為了這點而生的
參考C++11 tag_invoke的實作 duck_invoke

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
#include <bfg/tag_invoke.h>
namespace compute {
BFG_TAG_INVOKE_DEF(formula);
} // namespace compute

template <typename Compute>
float do_compute(const Compute & c, float a, float b)
{
return compute::formula(c, a, b);
}

struct custom_compute
{
private:
friend float
tag_invoke(compute::formula_t, const custom_compute &, float a, float b)
{
return a * b;
}
};

int main()
{
do_compute(custom_compute{}, 2, 3);
}

主要的作法是

  • 需要一個CPO參數,以上的範例是formula
  • 只需要一個tag_invoke function,不過可以Overloading,對不同的CPOj做不同的處理
    不過tag_invoke製造了其他問題,難以理解且囉嗦

Future

由於Executors跳票了,所以tag_invoke也不一定是最終解決方案
目前有其他提案,不過會不會被接受也在未定之天
詳細可以找找P2547R0來研究

Reference

如何理解 C++ 中的 定制点对象 这一概念?为什么要这样设计?
c++ execution 与 coroutine (一) : CPO与tag_invoke
C++特殊定制:揭秘cpo与tag_invoke!
Customization Points
Argument-dependent lookup - cppreference.com
Why tag_invoke is not the solution I want (brevzin.github.io)
What is a niebloid?
ADL,Concepts与扩展C++类库带来的思考
Duck Invoke — tag_invoke for C++11

寫了一堆CC++的文章,是時候換換口味了
Macro在Rust也有,不過不同於C/C++的Texture Replace
Rust的Macro強大的不得了,順便也跟C++的template做個比較

Declarative Macros

從min開始

C macro版的min或是C++ templaate版的就不提供了,寫到不想寫了
直接看Rust的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
macro_rules! min {
($a:ident, $b:ident) => {
if ($a < $b) {
$a
} else {
$b
}
}
}
fn main() {
let a = 2u32;
let b = 3u32;
println!("{}", min!(a, b));
}

這樣看起來沒什麼特別的
那如果多加一個變數呢

min version2

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
macro_rules! min {
($a:ident, $b:ident) => {
if ($a < $b) {
$a
} else {
$b
}
};
($a:ident, $b:ident, $c:ident) => {
if ($a < $b) {
if ($a < $c) {
$a
} else {
$c
}
} else {
if ($b < $c) {
$b
} else {
$c
}
}
}
}
fn main() {
let a = 3u32;
let b = 2u32;
let c = 1u32;
println!("{}", min!(a, b, c));
}

同樣的macro,可以有兩種不同的使用方式意
C語言的marco板本長這樣

1
2
3
4
5
#define min_2(a, b) ((a) < (b)) ? (a) : (b)
#define min_3(a, b, c) ((a) < (b)) ? ((a) < (c)) ? (a) : (c) : ((b) < (c)) ? (b) : (c)
#define GET_MACRO(_1,_2,_3,NAME,...) NAME
#define min(...) GET_MACRO(__VA_ARGS__, min_3, min_2)(__VA_ARGS__)
printf("%d\n", min(3, 2, 1));

看起來就是一堆亂七八糟拼湊的組合怪
來看看Template版

1
2
3
4
5
6
7
8
9
10
template <typename T>
T min(T a, T b)
{
return (a < b) ? a : b;
}
template <typename T>
T min(T a, T b, T c)
{
return (a < b) ? (a < c) ? a : c : (b < c) ? b : c;
}

憑藉於Function overloading,可讀性高很多,唯一比較麻煩的是要寫兩次template function declaration

min version3

來個varadic個版本,先寫個看起來沒問題,實際上編譯不過的

1
2
3
4
5
6
7
8
9
10
11
macro_rules! min {
($a:ident) => { $a };
($a:ident, $($b:ident),+) => {
let minV = min!($($b),+)
if ($a < minV) {
$a
} else {
minV
}
};
}

後來發現Rust Macro裡面不能有local variable,只能改成這樣

1
2
3
4
5
6
macro_rules! min {
($a:ident) => { $a };
($a:ident, $($b:ident),+) => {
std::cmp::min($a, min!($($b),+))
};
}

之後又發現一點和C/C++ preprocessor不同的地方,由於他是直接對AST做操作,所以得到的Token要自己Parse
所以做個實驗,參數之間分隔用;取代,,這樣是合法的

1
2
3
4
5
6
7
8
9
10
11
12
macro_rules! min {
($a:ident) => { $a };
($a:ident; $($b:ident);+) => {
std::cmp::min($a, min!($($b);+))
};
}
fn main() {
let a = 3u32;
let b = 2u32;
let c = 1u32;
println!("{}", min!(a; b; c));
}

不過沒辦法用local variable有點可惜,
Marco版的,我寫不出來,直接看Variadic Template的版本

1
2
3
4
5
6
7
8
9
10
template <typename T, typename... Args>
T min(const T& first, const Args&... args)
{
if constexpr (sizeof...(Args) == 0) {
return first;
} else {
const auto minV = min(args...);
return (first < minV) ? first : minV;
}
}

可以做更多的變化,不過Variadic Template最大的問題是我永遠記不住...到底要放哪這件事`
不過Rust真正厲害的是第二種Macro

Procedural Macros

基本上就是把輸入的TokenStream轉成另外的TokenStream的流程

分成三種

1
$ cargo new macro-demo --lib

Cargo.toml新增以下兩行

1
2
[lib]
proc-macro = true

Attribute macros

1
2
3
4
5
6
7
#[proc_macro_attribute]
fn sorted(args: TokenStream, input: TokenStream) -> TokenStream {
let _ = args;
let _ = input;

unimplemented!()
}

How to use atribute macro

1
2
3
4
5
6
#[sorted]
enum Letter {
A,
B,
C,
}

Function-like procedural macros

1
2
3
4
5
6
#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
let _ = input;

unimplemented!()
}

How to use function-like macro

1
2
3
seq! { n in 0..10 {
/* ... */
}}

Derive macro helper attributes

1
2
3
4
5
6
#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
let _ = input;

unimplemented!()
}

How to use derived macro

1
2
3
4
#[derive(Builder)]
struct Command {
// ...
}

Caution

Procedural Macros不同於Declarative Macros,必須單獨是一個crate存在,目前IDE對Proc Macro的支持度不好,連Debug Proc Macro也很麻煩,最常使用的還是print大法

Simple example

從別人的範例中學來的,這邊實作一個Attribute macros

1
2
3
4
5
6
7
$ mkdir rust_proc_macro_demo && cd rust_proc_macro_demo
$ mkdir rust_proc_macro_guide && cd rust_proc_macro_guide
$ cargo init --bin
$ cd ..
$ mkdir proc_macro_define_crate && cd proc_macro_define_crate
$ cargo init --lib
$ cd ..

修改proc_macro_define_crate/Cargo.toml
加入

1
2
3
4
5
6
[lib]
proc-macro = true

[dependencies]
quote = "1"
syn = {features=["full","extra-traits"]}

接著修改rust_proc_macro_guide/Cargo.toml

1
2
[dependencies]
proc_macro_define_crate = {path="../proc_macro_define_crate"}

置換掉proc_macro_define_crate/src/lib.rs裡面的內容

1
2
3
4
5
6
7
8
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn mytest_proc_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
eprintln!("Attr {:#?}", attr);
eprintln!("Item {:#?}", item);
item
}

一樣將rust_proc_macro_guide/src/main.rs內部的內容換掉

1
2
3
4
5
6
use proc_macro_define_crate::mytest_proc_macro;

#[mytest_proc_macro(HungMingWu)]
fn foo(a:i32){
println!("hello world");
}

接著用cargo check檢查

1
2
$ cd rust_proc_macro_guide/
$ cargo check

可以看到類似這樣的輸出

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
Attr TokenStream [
Ident {
ident: "HungMingWu",
span: #0 bytes(69..79),
},
]
Item TokenStream [
Ident {
ident: "fn",
span: #0 bytes(82..84),
},
Ident {
ident: "foo",
span: #0 bytes(85..88),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
ident: "a",
span: #0 bytes(89..90),
},
Punct {
ch: ':',
spacing: Alone,
span: #0 bytes(90..91),
},
Ident {
ident: "i32",
span: #0 bytes(91..94),
},
],
span: #0 bytes(88..95),
},
Group {
delimiter: Brace,
stream: TokenStream [
Ident {
ident: "println",
span: #0 bytes(101..108),
},
Punct {
ch: '!',
spacing: Alone,
span: #0 bytes(108..109),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
kind: Str,
symbol: "hello world",
suffix: None,
span: #0 bytes(110..123),
},
],
span: #0 bytes(109..124),
},
Punct {
ch: ';',
spacing: Alone,
span: #0 bytes(124..125),
},
],
span: #0 bytes(95..127),
},
]

這樣我們就能看出Attr和Item分別對應的TokenStream了

From TokenStream to Syntax Tree

有些時候,光看Lexer的TokenStream無助於解決問題,我們需要Syntax Tree
因此我們修改mytest_proc_macro

1
2
3
4
5
6
7
8
9
10
11
use proc_macro::TokenStream;
use syn::{parse_macro_input, AttributeArgs, Item};
use quote::quote;

#[proc_macro_attribute]
pub fn mytest_proc_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
eprintln!("Attr {:#?}", parse_macro_input!(attr as AttributeArgs));
let body_ast = parse_macro_input!(item as Item);
eprintln!("Item {:#?}", body_ast);
quote!(#body_ast).into()
}

會跑出這樣的結果

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
Attr [
Meta(
Path(
Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "HungMingWu",
span: #0 bytes(69..79),
},
arguments: None,
},
],
},
),
),
]
Item Fn(
ItemFn {
attrs: [],
vis: Inherited,
sig: Signature {
constness: None,
asyncness: None,
unsafety: None,
abi: None,
fn_token: Fn,
ident: Ident {
ident: "foo",
span: #0 bytes(85..88),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
paren_token: Paren,
inputs: [
Typed(
PatType {
attrs: [],
pat: Ident(
PatIdent {
attrs: [],
by_ref: None,
mutability: None,
ident: Ident {
ident: "a",
span: #0 bytes(89..90),
},
subpat: None,
},
),
colon_token: Colon,
ty: Path(
TypePath {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "i32",
span: #0 bytes(91..94),
},
arguments: None,
},
],
},
},
),
},
),
],
variadic: None,
output: Default,
},
block: Block {
brace_token: Brace,
stmts: [
Semi(
Macro(
ExprMacro {
attrs: [],
mac: Macro {
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "println",
span: #0 bytes(101..108),
},
arguments: None,
},
],
},
bang_token: Bang,
delimiter: Paren(
Paren,
),
tokens: TokenStream [
Literal {
kind: Str,
symbol: "hello world",
suffix: None,
span: #0 bytes(110..123),
},
],
},
},
),
Semi,
),
],
},
},
)

Comparsion with C/C++

要達到類似的功能,除了X-Macros之外,我想不到類似的方法了
不過X-Marcos不僅醜,功能還有限,Debug更困難

Reference

Rust Macro 手册
Rust宏编程新手指南【Macro】
Rust 过程宏 101
The Little Book of Rust Macros
Rust Latam: procedural macros workshop
Macros in Rust: A tutorial with examples - LogRocket Blog
Overloading Macro on Number of Arguments

Story

故事起源來自於看到類似這樣的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
#define VL_RESTORER(var) \
const VRestorer<typename std::decay<decltype(var)>::type> restorer_##var(var);

template <typename T> class VRestorer {
T& m_ref;
const T m_saved;
public:
explicit VRestorer(T& permr)
: m_ref{permr}
, m_saved{permr} {}
~VRestorer() { m_ref = m_saved; }
};

利用RAII來保存上下文當前的值,執行到結束的時候恢復原狀
不過

1
2
3
4
5
int a = 1, b = 2;
VL_RESTORER(a);
VL_RESTORER(b);
a = 3;
b = 4;

用起來沒什麼問題,不過總要找個題目來練習

ScopeExit

基本上就是RAII的變形,在Destructor的部分執行我們需要的Function,隨便在github搜尋就一堆了,這邊有個最簡單的方案

1
2
3
4
5
6
7
8
9
10
11
12
template <typename F>
struct scope_exit
{
F f;
~scope_exit() { f(); }
};

template <typename F>
inline scope_exit<F> make_scope_exit(F&& f)
{
return scope_exit<F>{f};
}

如果使用上C++17的CTAD,底下的make_scope_exit也不一定得存在

所以問題就變成了這樣,我希望在結束的時候,將所存的變數恢復原狀
問題就變成了該怎麼做

Higher Order Function

雖然C++不是標準的Functional Programming Language,不過要做點手腳還是辦得到的
問題變成了,傳入需要保存狀態的變數,回傳是一個函數,執行這個函數就能恢復原狀,這裡用上了Variadic Template和Tuple

1
2
3
4
5
6
7
8
9
template <typename ...Ts>
inline auto restore(Ts&& ...ts)
{
return [restore_ref = std::tuple<std::add_lvalue_reference_t<std::decay_t<Ts>>...>(std::forward<Ts>(ts)...),
store = std::tuple<std::add_const_t<std::decay_t<Ts>>...>(ts...)]() mutable noexcept
{
restore_ref = store;
};
}

這邊有兩個tuple,其中restore_ref保存了所有變數的reference,store則是變數這個時間點的值

Combo

上面的方式能夠寫成

1
2
3
4
int a = 1, b = 2;
auto _ = make_scope_exit(restore(a, b));
a = 3;
b = 4;

好壞就見仁見智了

過年前要生產出一些東西出來,不然太久沒寫文章了
看到Unsigned integer overflow和underflow造成的問題,覺得Rust的解法實在很好,在編譯時就能檢查出來

1
2
3
4
5
6
7
8
9
#![deny(clippy::integer_arithmetic)]

use std::env;
const PAGE_SIZE: u64 = 4096;
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
let size: u64 = args[0].parse().unwrap();
println!("({} - 2 - {}) => {}", PAGE_SIZE, size, PAGE_SIZE - 2 - size);
}

然後安裝clippy當作cargo的subcommand

1
2
3
4
5
6
7
$ cargo clippy
error: integer arithmetic detected
--> src/main.rs:8:54
|
8 | println!("({} - 2 - {}) => {}", PAGE_SIZE, size, PAGE_SIZE - 2 - size);
| ^^^^^^^^^^^^^^^^^^^^
|

如果要正確的處理,程式碼大概是這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
fn foo(len: u64, size: u64) {
match (PAGE_SIZE - 2).checked_sub(size) {
Some(capacity) if len > capacity => {
println!("no capacity left");
}
Some(capacity) => {
println!("sufficient capacity {}", capacity);
}
None => {
println!("underflow! bad user input!");
}
}
}

等價的C語言表示方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define IS_UINT_SUB_UNDERFLOW(x, y) ((x) - (y) > (x))
#define IS_UINT_ADD_OVERFLOW(x, y) ((x) + (y) < (x))
#define IS_UINT_MUL_OVERFLOW(x, y, size_max) ((x) && (y) > (size_max) / (x))
#define PAGE_SIZE 4096u

void foo(unsigned int len, unsigned int size) {
if (IS_UINT_SUB_UNDERFLOW(PAGE_SIZE - 2, size)) {
printf("underflow! bad user input!\n");
} else {
unsigned int capacity = PAGE_SIZE - 2 - size;
if (len > capacity) {
printf("no capacity left\n");
} else {
printf("sufficient capacity %u\n", capacity);
}
}
}

不過這需要CPU實作Modular arithmetic而不是Saturation arithmetic

不然還是有像Integers)這樣的第三方library,不過易用性就不如Rust了

Reference

Rust: detect unsigned integer underflow

Why Refelection

有些時候,我們需要遍歷struct/class的member,最常見的的用途就是print/serialization/deserialization

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

void print(const obj& o)
{
printf("%d\n", o.a);
}

這樣子的做法雖然直接,不過有幾個問題

  • 只要structure改變,你的implementation就要跟著改變
  • 假設要一直支持新的structure,我們需要一個新的overload function

另外有時候我們也需要 struct field name的資訊,例如我們想知道struct file的名稱,而Compiler編譯出來的程式碼沒有struct/class的field資訊,所以我們會這樣手動寫死

1
2
3
4
void print(const obj& o)
{
printf("a: %d\n", o.a);
}

如果我們把a名稱改成a1,也是要手動維護程式碼,那有什麼適合的方案嗎

Compilier dependent solution

clang的__builtin_dump_struct
只支援dump功能,其他沒了,也只有clang能用

1
2
3
4
5
6
7
8
9
10
struct obj1 {
int a;
int b;
};

int main() {
struct obj1 o = { .a=1, .b=2 };
__builtin_dump_struct(&o, &printf);
return 0;
}

Wrong Idea

想到最直覺的方法,當然是這樣寫

1
2
3
4
5
6
template <typename T>
void print(const T& o)
{
for (auto& field : { field of o })
std::cout << field << "\n";
}

不過眾所周知,for loop不能這樣用

Boost pfr for resuce

山不轉路轉,有無數的聰明人想出了方法,其中最有名的就是boost pfr

1
2
3
4
5
6
7
8
#include <boost/pfr/ops.hpp>
template <typename T>
void print(const T& o)
{
boost::pfr::for_each_field(o, [&](const auto& v) {
std::cout << v << "\n";
});
}

不過這方法也是有其侷限性

  • 增加了對 boost pfr的依賴
  • 只能對Aggregate type使用
  • 不能解決field name的問題

    nameof

    一個借鑑於C#的library
    大概的用法是這樣子
    1
    2
    NAMEOF(somevar) -> "somevar"
    NAMEOF(person.address.zip_code) -> "zip_code"
    對單一變數效果還行,不過對struct/class裡面的field name還是無能為力

Macro based Solution

以Boost Hana為例

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
#include <boost/hana.hpp>
struct OrderedItem {
BOOST_HANA_DEFINE_STRUCT(
OrderedItem,
(std::string, item_name),
(int64_t, quantity),
(int64_t, price_cents)
);
};

template<typename T>
boost::json::value FormatStructure(const T &t) {
boost::json::object result;
boost::hana::for_each(t, boost::hana::fuse([&result](auto name, auto member) {
result.emplace(boost::hana::to<const char *>(name), FormatObject(member));
}));
return result;
}
template<typename T>
boost::json::value FormatObject(const T &t) {
if constexpr (boost::hana::Struct<T>::value) {
return internal::FormatStructure(t);
} else {
return internal::FormatValue(t);
}
}

光看程式碼就猜的到,BOOST_HANA_DEFINE_STRUCT做了很多事情,維護每個除了原先的 field declaration之外,還維護了field name的資訊
不過Macro就是黑魔法,維護起來就是麻煩,不過現階段也沒更好的方法

Runtime Refelection

上面說的都是Compile-time Refelection,當然還有一派作法是在Runtime時做Refelection,能無視編譯器的差異,提供比編譯器更多的Metadata,不過這一切都是要手動做

不管Compile-time Refelectionc還是Runtime Refelection,都掙脫不了Macro和Template的禁錮

Future

有個實驗性質的reflection TS

1
2
3
4
5
6
7
8
9
10
11
struct S {
int b;
std::string s;
std::vector<std::string> v;
};
 
// Reflection TS
#include <experimental/reflect>
using meta_S = reflexpr(S);
using mem = std::reflect::get_data_members_t<meta_S>;
using meta = std::reflect::get_data_members_t<mem>;

不過前途未卜啊,搞不好像NetworkTS那樣推倒重來,C++23是無望了

Reference

What is array of structure

這就是我們一般常用的模式

1
2
3
4
struct Obj {
int a, b;
};
std::array<Obj, 100> objs;

What is structure of array

剛好和上面的觀念相反,將object的member集中在一起,以上面的例子來說,可以寫成這樣

1
std::tuple<std::array<int, 100>, std::array<int, 100>> objs;

Why structure of array

從上面兩個寫法看來,array of structure更為自然,容易咧解
那為什麼會有structure of array的出現,一切都是為了性能
例如這樣子的Code

1
2
3
int sum = 0;
for (auto v : objs)
sum += v.a;

由於CPU locality特性,a的stride是sizeof(Obj)大小,所以CPU Cache幾乎沒有作用
但如果寫成這樣

1
2
3
4
int sum = 0;
auto &as = std::get<0>(objs);
for (auto v : as)
sum += v;

由於std::array<int, 100>是個連續的memory area,因此在CPU locality方面比起上面方案好
不過有一好沒兩好
structure of array的缺點有

  • 程式碼不容易讀

How to use struct of array in C++

由於C++沒有原生的SOA支援,有第三方的Library供使用

不過C++ Refelction何時落地啊

雖然之前有看過,不過看過即忘,還是得寫下來

What’s projection

從一個寫到爛的範例開始

1
2
3
4
5
6
7
8
struct Person {
std::string name;
int age;
};
std::vector<Person> persons;
std::sort(begin(persons), end(persons), [](const auto& p1, const auto& p2) {
return p1.name < p2.name;
});

相信這樣的程式碼已經寫到吐了
如果用C++20 Ranges寫的話可以這樣寫

1
std::ranges::sort(persons, std::ranges::less{}, &Person::name);

可以知道我們要比的就是name,而這樣的寫法就叫做Projection

Backport to C++17

其實要backport到更之前的版本也行, 只要有第三方或是自己寫的invoke
然後寫一個projecting_fn functor,compose以下的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename Function, typename Projection>
class projecting_fn {
public:
projecting_fn(Function function, Projection projection)
: m_function{ std::move(function) }
, m_projection{ std::move(projection) }
{
}

template <typename... Args>
decltype(auto) operator() (Args&&... args) const
{
return std::invoke(
m_function,
std::invoke(m_projection, std::forward<decltype(args)>(args))...);
}

private:
Function m_function;
Projection m_projection;
};
std::sort(begin(persons), end(persons),
projecting_fn{ std::less{}, &Person::name });

Projection and view filter

像這樣的Source Code是無法通過編譯的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
struct LessThan
{
bool operator()(const T& x){
return x < value;
}
T value;
};

struct Apple
{
int weight;
int size;
};

int main()
{
auto apples = std::vector<Apple>{{1,2}, {2,3}, {3,4}};

auto smallApples = apples | views::filter(LessThan{3}, &Apple::size);
}

解決方式有兩種

不用Projection也是一種解決方法

1
apples | views::filter([] (Apple& a) {return a.size < 3;})

不過這方式就與本文無關了

Boost HOF

1
apples | views::filter(hof::proj(&Apple::size, LessThan{3}));

這方法就類似上面C++17的projecting_fn

Reference

The simplest module

先看範例,就是Module版的Hello World

1
2
3
export module hello;
export void hello_world() {};
void non_export_func() {}

而Consumer Module的一方就這樣寫

1
2
3
4
5
6
7
import hello;
int main()
{
hello_world(); // OK
non_export_func(); // Cannot compile
return 0;
}

Description

從這個範例當中,Consumer這邊不用特別說
這邊要說的是如何寫個Module

Module Unit

在C++20,有了一個新的Compile Unit,就是Module Unit,所有Module Unit的Top Level Statement都是有module關鍵字的
module前面有沒有export就是決定這是哪一種Module Unit

  • export的叫作Module Interface Unit
  • export的叫做Module Implementation Unit

Module Implementation Unit後面再說

The content of a module

一個Module擁有

  • 一個以上的Module Interface Unit
  • 零個以上的Module Implementation Unit

且每個Module裡面有且唯一一個Primary Module Interface Unit

在Hello World這個範例當然只有Primary Module Interface Unit 的存在,至於什麼是Primary Module Interface Unit,也是後面再說

export

在上面的範例,我們定義了兩個函數

1
2
export void hello_world() {};
void non_export_func() {}

不塗於傳統的header file方式,如果是傳統的header file,兩個function應該都可以被外界可見,而Module Unit只有export出的符號才能輩Connsumer看到
export的其他用法還有這樣

1
2
3
4
5
6
7
8
// export entire namespace
export namespace hello {}

// export the symbols in the block
export {
int e = 1;
void test() {}
}

Module Implementation Unit

就像傳統header/implementation的方法,我們可以把declaration/implementation分離,因此我們有了Module Implementation Unit
重寫我們的範例,將implementation分開
因此我們的Module Interface Unit就變成

1
2
3
export module hello;
export void hello_world();
void non_export_func();

而Module Implementation Unit則是

1
2
3
module hello;
void hello_world() {};
void non_export_func() {}

如同之前所說的,module前面沒加export的就是Module Implementation Unit,而在function implementation前面也沒加export,就跟傳統的方式很像

My thought on Module Implementation Unit

之前declaration/implementation被人詬病的一點,就是你要維護兩份狀態,當你declaration改了之後,如果implementation沒改,會產生不可預料的後果,運氣好的話是編譯不過,運氣不好產生深層的Bug更難解

如同之前所說的,一個Module可以不必擁有Module Implementation Unit
那存在的必要是什麼?

我認為是將舊有的Source Code Mitigation到C++ Module的方式
如同現在流行的header only library一樣,未來的Module應該僅由Module Interface Unit組成

Import other module

寫Module時不免使用到其他Module,讓我們定義一個新的Module

1
2
export module world;
export struct obj {};

而我們的hello module就變成這樣

1
2
3
export module hello;
import world;
export void hello_world(obj o) {};

注意,import只能放在top level module declaration之下,不能交換順序

接著要回去看Consumer的部分了

Visibility control

此時我們的Consumer會是這樣

1
2
3
4
5
6
7
8
import hello;
import world;
int main()
{
obj o;
hello_world(o);
return 0;
}

這裡該注意的點,在hello module當中雖然import了world,但是不
會再次輸出symbol到hello module metadata中
因此如果Consumer沒加上import world時,會發現找不到obj的情形

但如果我們將hello改成這樣

1
2
3
export module hello;
export import world;
export void hello_world(obj o) {};

這邊將我們import進來的Module再度export出去,這也是我們細分module的基礎
那麼Consumer不加import world也是可以正常運行

Divide module into small parts

當一個Module大起來之後,要降低複雜度,細分成更小的Block是需要的,而其中又有兩種方法

Sobmodule

我們將hello_world分成兩個function
一個放在hello.sub_a,另外一個放在hello.sub_b
直接看程式碼

1
2
export module hello.sub_a;
export void hello() {};

而另外一個就不貼了,看看我們hello module的定義

1
2
3
export module hello;
export import hello.sub_a;
export import hello.sub_b;

Reexport出hello.sub_ahello.sub_b的exported symbol

Note

hello.sub_ahello_sub_b是各自獨立完整的Module,submodule機制只是邏輯組合,讓他們看起來像是同一個Module
所以你Consumer這樣寫也是可以的

1
2
3
4
5
6
7
8
import hello.sub_a;
import hello.sub_b;
int main()
{
hello();
world();
return 0;
}

Module partition

不同於submodule,partition所分的sub partition不能個別存在
一樣直接看程式碼

1
2
export module hello:part_a;
export void hello() {};

跟上面很像,不過將.改成了:
而我們的hello module則是

1
2
3
export module hello;
export import :part_a;
export import :part_b;

這邊有幾點要注意的

  • 一個module name當中沒有:出現的就是Primary Module Interface Unit,如同之前所說
    一個以上的Module Interface Unit,有且唯一一個Primary Module Interface Unit
    這個範例有三個Module Interface Unit,只有hello是Primary Module Interface Unit
    hello.sub_a則是一個獨立的Module,只是邏輯上看起來是同一個Mdoule

  • Partition只能接受import :part_a的語法,import hello:part_a是不對的

  • Consumer只能寫import hello

Global Module Fragment

Global Module Fragment是提供preprocessor使用的空間,因此你可以在這邊定義Marco,或是include未被moduleized的header file,而在這邊定義的symbol則不會輸出到module interface中,因此不會汙染全局環境

Global Module Fragment必須在export module之前,就像這樣

1
2
3
4
5
module;
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#include <string>
#include <vector>
export module hello;

Reference

前言

基本上這個要求蠻奇怪的,ASIO又不是沒提供Synchronize API,不過有些事情就是只有Asynchronous API能做到
例如我要在五秒鐘之內連線,五秒鐘之內無法連上就直接結束,如果用Synchronize API,Timeout由作業系統決定
這個時候就只有自己寫了

use_future

ASIO有一個feature,可以將Async operation轉成Sync operation
一般來說我們的程式碼會寫成這樣

1
2
3
socket.async_connect(endpoint, [](std::error_code ec) {
// blablabla
});

但是如果我們用use_future的話,ASIO內部會自己轉成promise/future的Pattern
這適合在Threead synchronize的情景使用

1
2
3
4
5
6
7
asio::io_context ctx;
asio::ip::tcp::socket socket(ctx);
auto future = socket.async_connect(endpoint, asio::use_future);
std::thread t([&] {
ctx.run();
});
future.get();

Combie with C++20 Coroutine

如果我們的條件更複雜,如一開始寫的五秒鐘Timeout這件事,上面的程式碼就不敷使用,
如果用原先的Function callback方式寫大概會死一堆腦細胞,而Coroutine可以讓我們大大減輕心智負擔

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
asio::awaitable<void> timeout(std::chrono::seconds seconds)
{
asio::steady_timer timer(co_await asio::this_coro::executor);
timer.expires_after(seconds);
co_await timer.async_wait(use_nothrow_awaitable);
}

asio::awaitable<std::error_code> connect_with_timeout(
asio::ip::tcp::socket& socket,
const asio::ip::tcp::endpoint& endpoint)
{
using namespace asio::experimental::awaitable_operators;
auto result = co_await(
socket.async_connect(endpoint, use_nothrow_awaitable) ||
timeout(std::chrono::seconds(5))
);
if (result.index() == 1) {
co_return asio::error::timed_out; // timed out
}
auto [r] = std::get<0>(result);
co_return r;
}

asio::io_context io_context;
auto connect_future = asio::co_spawn(
io_context.get_executor(),
connect_with_timeout(asio::ip::tcp::socket(io_context), endpoint),
asio::use_future);
io_context.run();
return connect_future.get();

如上面程式碼寫的一樣
connect_with_timeout有兩種可能,一個是socket connect的結果,另外一個是timeout
asio::co_spawn的最後一個參數不是教學中的detach,而是剛剛講的use_future
這樣子就可以把Coroutine 和 promise/future一起使用