0%

原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
struct Test {
int index;
std::string name;
void printInfo() const {
std::cout << "index: " << index << ", name: " << name << "\n";
}
};
int main()
{
Test test;
test.index = 1;
test.name = "test_1";
test.printInfo();
auto index_addr = &Test::index;
auto name_addr = &Test::name;
auto fun_print_addr = &Test::printInfo;
test.*index_addr = 2;
test.*name_addr = "test_2";
(test.*fun_print_addr)();
return 0;
};

透過上面的index_addrname_addrfun_print_addr等,可以對object進行操作
而反射主要分成兩部分

  • Metadata generation
    和C++ object有關的information就叫做metadata,如上面的例子,這邊的困難點是如何減少工作量
  • Metadata Reflection
    既然有了Metadata,如何跟現實使用上連結起來

雖然目前的官方標準還沒出來,不過現在有兩大流派

手工打造

什麼辦不到的事情,用Marco就好了
以Boost Describe舉例

1
2
3
4
5
6
struct X
{
int m1;
int m2;
};
BOOST_DESCRIBE_STRUCT(X, (), (m1, m2))

其他Macro Based的方案也差不多,就是另外定義一個Macro,自動生成類似上面的Metadata
不過這邊的問題就是

  • 你要同時維護兩份資料的一致性
  • Macro滿天飛
  • 修改困難 (因為都是Marco的黑魔法,要新增功能就得對Marco動刀)

libclang

另外一派就是借助libclang來動手生成,透過Parse C++ AST來生成需要的API
舉例說明

1
2
3
4
5
6
7
8
class MyClass
{
public:
int field = 0;
static int static_field;
void method();
static void static_method();
};

生成的Metadata可以這麼使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
reflang::Class<MyClass> metadata;
MyClass c;

// Modify / use c's 'field'.
reflang::Reference ref = metadata.GetField(c, "field");
ref.GetT<int>() = 10;

// Modify / use 'static_field'.
ref = metadata.GetStaticField("static_field")
ref.GetT<int>() = 10;

// Execute 'method()'.
auto methods = metadata.GetMethod("method");
(*methods[0])(c);

// Execute 'static_method()'.
auto methods = metadata.GetStaticMethod("static_method");
(*methods[0])();

這個方案的問題在於

  • 要有libclang才能用
  • 構建的時候會多一個步驟,必須掃描所有的檔案,生成需要的header/sources,修改Makefile/CMakeLists.txt來調整編譯流程

Reflection API in the future

雖然現有的Reflection library多的跟山一樣,不過眾口難調,有些是針對特定用途設計的,無法涵蓋其他方面的使用,有些功能完整,但是難用
於是乎就有人想要對語法方面下手,成為C++ Standard中的一部分

1
2
3
4
5
6
7
8
template <class T>
void print_type() {
std::cout << "void "
<< get_name_v<reflexpr(print_type<T>)> // guaranteed "print_type"
<< "() [with T = "
<< get_display_name_v<reflexpr(T)>
<< "]" << std::endl;
}

reflexpr和decltype一樣是type-based,所以可以套用到type based metaprogramming中
不過會不會成為標準是另外一回事了
跟Network Library一樣,成為標準之前先用成熟的方案解決

Reference

How to write comparsion operator for custom type

The simple case

假設我們有一個類別

1
2
3
struct Value {
int v;
};

我們要怎麼寫出的程式碼

1
2
Value v1, v2;
v1 < v2;

有幾種方式

Naive solution

一種是當member function存在
手動寫出所有comparsion operator

1
2
3
4
5
6
struct Value {
int v;
bool operator<(const Value &rhs) { return v < rhs.v; }
bool operator==(const Value &rhs) { return v == rhs.v; }
// Ignore
};

另外一種是Free function存在

1
2
bool operator<(const Value &lhs, const Value &rhs) { return lhs.v < rhs.v; }
bool operator==(const Value &lhs, const Value &rhs) { return lhs.v == rhs.v; }

兩種實現原理相同,看情況選擇要用哪種,現在要討論的是其他的問題
當我們需要支持更多運算符號時,我們就需要寫更多的Function

1
2
3
bool operator>(const Value &lhs, const Value &rhs);
bool operator==(const Value &lhs, const Value &rhs);
bool operator!=(const Value &lhs, const Value &rhs);

如果我們需要支援另外一種Type

1
2
3
4
struct Value1 {
int v;
int v1;
};

然後又要出現一堆複製貼上加上手動修改的產物

1
2
3
4
bool operator<(const Value1 &lhs, const Value1 &rhs);
bool operator>(const Value1 &lhs, const Value1 &rhs);
bool operator==(const Value1 &lhs, const Value1 &rhs);
bool operator!=(const Value1 &lhs, const Value1 &rhs);

寫起來麻煩又沒什麼技術含量

CRTP solution

有些operator可以用其他operator表示,例如Not Equal就是Not + Equal
所以我們可以用CRTP技巧減少我們的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class Derived>
struct Equality {
bool operator !=(const Equality &rhs) {
return !(static_cast<Derived&>(*this) == static_cast<const Derived&>(rhs));
}
};

struct Value : Equality<Value> {
int v;
bool operator==(const Value &rhs) const { return v == rhs.v; }
};

struct Value1 : Equality<Value1> {
int v;
int v1;
bool operator==(const Value1 &rhs) const { return v == rhs.v; && v1 == rhs.v1; }
};

其他的operator可以如法炮製,很多的C++ Graphics/Math Library都用了這個技巧
只要實作<==,可以用來推導出其他四種比較關係
不過很不直觀,CRTP就是一種Hack,那有沒有更好的方法

C++20 spaceship operator

Spaceship oerator也叫做The Three-Way Comparison Operator
這是C++20的一個特性,直接上Code來說明

1
2
3
4
5
#include <compare>
struct Value {
        int v;
        auto operator<=>(const Value&) const = default; (1)
};

而Compiler直接為你生成Comparsion Code,原先的程式碼視為這樣

1
2
3
(a <=> b) < 0  //true if a < b
(a <=> b) > 0 //true if a > b
(a <=> b) == 0 //true if a is equal/equivalent to b

這種方式類似於strcmp,會回傳<0>00三種情形
基本上這樣就滿足了80%的需求了,不過人生最難的就是那個But
有需要的話自定義比較方式的話,可以自定義comparsion operator

1
2
3
4
5
6
7
8
9
10
11
struct Value1 {
int v;
int v1;
public:
auto operator<=>(const Value1& rhs) const {
   if (auto cmp = v <=> rhs.v; cmp != 0)
   return cmp;
return v1 <=> rhs.v1;
}
 }
};

不過現在spaceship operator必須回傳的是std::strong_orderingstd::weak_orderingstd::partial_ordering其中之一
至於三種ordering的差異,在此不探討,需要的話去Reference看,大部分只需要std::strong_ordering即能完成需求

Reference

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