0%

今天才知道這個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 ;
    });
}

這裡的Resouce不光指Memory,可能是FILE,或是ffmpeg那種Handle
Resource Management一直都是個討論的重點,要混合在C++使用,有很多種方法
拿FILE來舉例好了

什麼都不做

1
2
3
FILE *fp = fopen(...);
// Do somthing
fclose(fp);

這種方法最直接,不用學其他額外的方法,不過常常會因為程式碼的改變,而忘記release resource這件事,因此才有其他流派生存的機會

defer

大概的程式碼長這樣,不過在C++不一定叫defer,可能叫ScopeGuard之類的東西,不過原理是一樣的

1
2
FILE *fp = fopen(...);
defer([&]() { fclose(fp); });

在小規模的使用是沒問題的,當Resoruce 一多就會變得冗餘,例如

1
2
3
4
5
6
FILE *fp1 = fopen(...);
defer([&]() { fclose(fp1); });
FILE *fp2 = fopen(...);
defer([&]() { fclose(fp2); });
FILE *fp3 = fopen(...);
defer([&]() { fclose(fp3); });

於是C++ RAII的方式出現了,有鑑於shared_ptr耗費較多的資源,這邊的方案都是unique_ptr為主

naive unique_ptr solution

為每個resource寫出一個Wrapper

1
2
3
4
5
6
struct FILEWrapper {
FILE* f;
FILEWrapper(FILE *file) : f(file) {}
~FILEWrapper() { if (f) fclose(f); }
};
std::unique_ptr<FILEWrapper> fp;

沒什麼不好,只是工作量太大,每加一種Resource就要有個Wrapper,那有沒有其他方案

unique_ptr with custrom destruction

同樣以FILE舉例,新增一個function object

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <memory>
struct FileCloser {
void operator()(FILE *f) {
if (f) fclose(f);
};
};
std::unique_ptr<FILE, FileCloser> fp;

這樣看起來跟上面差不了多少
另一種方法是

1
std::unique_ptr<FILE, int(*)(FILE *)> fp(fp, fclose);

這種方式比上面那個還差

out_ptr

雖然跟上面無關,不過這也是unique_ptr的一部分,一併提出
由於API設計的關係,input需要的是double pointer
程式有些可能會變成這樣

1
2
3
4
5
std::unique_ptr<ITEMIDLIST_ABSOLUTE, CoTaskMemFreeDeleter> pidl; ITEMIDLIST_ABSOLUTE* rawPidl;
hr = SHGetIDListFromObject(item, &rawPidl);
pidl.reset(rawPidl);
if (FAILED(hr))
return hr;

這時候就是out_ptr使用場警

1
2
3
4
std::unique_ptr<ITEMIDLIST_ABSOLUTE, CoTaskMemFreeDeleter> pidl;
hr = SHGetIDListFromObject(item, std::out_ptr(pidl));
if (FAILED(hr))
return hr;

雖然這是在C++23才進入標準庫,不過
GitHub - soasis/out_ptr: Repository for a C++11 implementation of std::out_ptr (p1132), as a standalone library!
已經可以先嘗鮮了

template auto

C++17之後,放寬template的要求
於是這樣的程式碼成為可能

1
2
3
4
template <auto destroy>
struct c_resource {
};
c_resource<fclose> fp;

配合上C++20的Concept之後,成為威力強大的武器
以下是從Meeting CPP 2022中節錄出來的片段

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
template <typename T, auto * ConstructFunction, auto * DestructFunction>
struct c_resource {
using pointer = T *;
using const_pointer = std::add_const_t<T> *;
using element_type = T;

private:
using Constructor = decltype(ConstructFunction);
using Destructor = decltype(DestructFunction);

static_assert(std::is_function_v<std::remove_pointer_t<Constructor>>,
"I need a C function");
static_assert(std::is_function_v<std::remove_pointer_t<Destructor>>,
"I need a C function");

static constexpr Constructor construct = ConstructFunction;
static constexpr Destructor destruct = DestructFunction;
static constexpr T * null = c_resource_null_value<T>;

struct construct_t {};

public:
static constexpr construct_t constructed = {};

[[nodiscard]] constexpr c_resource() noexcept = default;
[[nodiscard]] constexpr explicit c_resource(construct_t) noexcept
requires std::is_invocable_r_v<T *, Constructor>
: ptr_{ construct() } {}

template <typename... Ts>
requires(sizeof...(Ts) > 0 && std::is_invocable_r_v<T *, Constructor, Ts...>)
[[nodiscard]] constexpr explicit(sizeof...(Ts) == 1)
c_resource(Ts &&... Args) noexcept
: ptr_{ construct(static_cast<Ts &&>(Args)...) } {}

template <typename... Ts>
requires(sizeof...(Ts) > 0 &&
requires(T * p, Ts... Args) {
{ construct(&p, Args...) } -> std::same_as<void>;
})
[[nodiscard]] constexpr explicit(sizeof...(Ts) == 1)
c_resource(Ts &&... Args) noexcept
: ptr_{ null } {
construct(&ptr_, static_cast<Ts &&>(Args)...);
}

template <typename... Ts>
requires(std::is_invocable_v<Constructor, T **, Ts...>)
[[nodiscard]] constexpr auto emplace(Ts &&... Args) noexcept {
_destruct(ptr_);
ptr_ = null;
return construct(&ptr_, static_cast<Ts &&>(Args)...);
}

[[nodiscard]] constexpr c_resource(c_resource && other) noexcept {
ptr_ = other.ptr_;
other.ptr_ = null;
};
constexpr c_resource & operator=(c_resource && rhs) noexcept {
if (this != &rhs) {
_destruct(ptr_);
ptr_ = rhs.ptr_;
rhs.ptr_ = null;
}
return *this;
};
constexpr void swap(c_resource & other) noexcept {
auto ptr = ptr_;
ptr_ = other.ptr_;
other.ptr_ = ptr;
}

static constexpr bool destructible =
std::is_invocable_v<Destructor, T *> || std::is_invocable_v<Destructor, T **>;

constexpr ~c_resource() noexcept = delete;
constexpr ~c_resource() noexcept
requires destructible
{
_destruct(ptr_);
}
constexpr void clear() noexcept
requires destructible
{
_destruct(ptr_);
ptr_ = null;
}
constexpr c_resource & operator=(std::nullptr_t) noexcept {
clear();
return *this;
}

[[nodiscard]] constexpr explicit operator bool() const noexcept {
return ptr_ != null;
}
[[nodiscard]] constexpr bool empty() const noexcept { return ptr_ == null; }
[[nodiscard]] constexpr friend bool have(const c_resource & r) noexcept {
return r.ptr_ != null;
}

auto operator<=>(const c_resource &) = delete;
[[nodiscard]] bool operator==(const c_resource & rhs) const noexcept {
return 0 == std::memcmp(ptr_, rhs.ptr_, sizeof(T));
}

#if defined(__cpp_explicit_this_parameter)
template <typename U, typename V>
static constexpr bool less_const = std::is_const_v<U> < std::is_const_v<V>;
template <typename U, typename V>
static constexpr bool similar = std::is_same_v<std::remove_const_t<U>, T>;

template <typename U, typename Self>
requires(similar<U, T> && !less_const<U, Self>)
[[nodiscard]] constexpr operator U *(this Self && self) noexcept {
return std::forward_like<Self>(self.ptr_);
}
[[nodiscard]] constexpr auto operator->(this auto && self) noexcept {
return std::forward_like<decltype(self)>(self.ptr_);
}
[[nodiscard]] constexpr auto get(this auto && self) noexcept {
return std::forward_like<decltype(self)>(self.ptr_);
}
#else
[[nodiscard]] constexpr operator pointer() noexcept { return like(*this); }
[[nodiscard]] constexpr operator const_pointer() const noexcept {
return like(*this);
}
[[nodiscard]] constexpr pointer operator->() noexcept { return like(*this); }
[[nodiscard]] constexpr const_pointer operator->() const noexcept {
return like(*this);
}
[[nodiscard]] constexpr pointer get() noexcept { return like(*this); }
[[nodiscard]] constexpr const_pointer get() const noexcept { return like(*this); }

private:
static constexpr auto like(c_resource & self) noexcept { return self.ptr_; }
static constexpr auto like(const c_resource & self) noexcept {
return static_cast<const_pointer>(self.ptr_);
}

public:
#endif

constexpr void reset(pointer ptr = null) noexcept {
_destruct(ptr_);
ptr_ = ptr;
}

constexpr pointer release() noexcept {
auto ptr = ptr_;
ptr_ = null;
return ptr;
}

template <auto * CleanupFunction>
struct guard {
using cleaner = decltype(CleanupFunction);

static_assert(std::is_function_v<std::remove_pointer_t<cleaner>>,
"I need a C function");
static_assert(std::is_invocable_v<cleaner, pointer>, "Please check the function");

constexpr guard(c_resource & Obj) noexcept
: ptr_{ Obj.ptr_ } {}
constexpr ~guard() noexcept {
if (ptr_ != null)
CleanupFunction(ptr_);
}

private:
pointer ptr_;
};

private:
constexpr static void _destruct(pointer & p) noexcept
requires std::is_invocable_v<Destructor, T *>
{
if (p != null)
destruct(p);
}
constexpr static void _destruct(pointer & p) noexcept
requires std::is_invocable_v<Destructor, T **>
{
if (p != null)
destruct(&p);
}

pointer ptr_ = null;
};

幾乎修正了上面所說的痛點
使用上也只要

1
c_resource<FILE, fopen, fclose> fp;

算是目前看到最通用的解法

Coroutine solution

這算是另闢新徑的方案,RAII的方案都把release resource放在destructor中
自從C++20引進Corotuine,產生了新的可能
使用上大概會是這樣

1
2
3
4
5
6
7
8
9
10
co_resource<FILE*> usage() {
FILE *fp = fopen(...);
co_yield fp;
fclose(fp);
}

void foo() {
co_resource<FILE*> r = usage();
// Do somthing
}

Reference

Allocator for C++11

滿足C++11中對Alloocator的需求,所能寫出的最簡單allocator
注意

  • 這邊的allocatte和deallocate不會呼叫Constructor/Destructor,只是單純的記憶體分配,為了簡單,直接用malloc/free
  • 可以對兩個Allocator做比較的動作,如果兩者相等的話,可以達成在A進行allocate,而在B進行deallocate的動作
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 <cstdlib>

template <typename T>
class Minallocator {
public:
using value_type = T;

T* allocate(size_t num) { return allocate(num, nullptr); }
T* allocate(size_t num, const void* hint) { return reinterpret_cast<T*>(std::malloc(sizeof(T) * num)); }
void deallocate(T* ptr, size_t num) { std::free(ptr); }
Minallocator() = default;
~Minallocator() = default;
Minallocator(const Minallocator&) = default;
Minallocator(Minallocator&&) = default;
Minallocator& operator=(const Minallocator&) = default;
Minallocator& operator=(Minallocator&&) = default;
};

template <typename T1, typename T2>
bool operator==(const Minallocator<T1>& lhs,const Minallocator<T2>& rhs)
{
return true;
}

template <typename T1, typename T2>
bool operator!=(const Minallocator<T1>& lhs, const Minallocator<T2>& rhs)
{
return false;
}

而要用自己的Allocate就可以這麼做

1
std::vector<int, Minallocator<int>> v;

std::scoped_allocator_adaptor

不常用,有用到再說

rebind

已知T類型的Allocator,想要根據相同策略拿到U類型的Allocator
也就是說希望用同樣的方式來分配U
可以透過

1
allocator<U>=allocator<T>::rebind<U>::other.

拿到,因此

std::allcoator<T>::rebind<U>::other等同於std::allcoator<U>
Myallcoator<T>::rebind<U>::other等同於Myallcoator<U>

在libstdc++中的實現

1
2
3
4
5
template <typename _Tp1>
struct rebind
{
typedef allocator<_Tp1> other;
};

Problem with allocators and containers

這樣的程式碼會有問題

1
2
3
4
ector<int, Minallocator<int>>  pool_vec  { 1, 2, 3, 4 };
vector<int, Other_allocator<int>> other_vec { };

other_vec = pool_vec;    // ERROR!

因為兩者的Allocator Type不同,所以直接複製不行,所以只要兩者相同就行了,也就是C++17 PMR的初衷

C++17 Polymorphic Memory Resource

新提出來的memory_resource是個asbtract class,不同的instance會有不同的行為
因此可以可以這樣做

1
2
3
4
5
6
7
8
9
10
11
12
13
// define allocation behaviour via a custom "memory_resource"
class my_memory_resource : public std::pmr::memory_resource { ... };
my_memory_resource mem_res;
auto my_vector = std::pmr::vector<int>(0, &mem_res);

// define a second memory resource
class other_memory_resource : public std::pmr::memory_resource { ... };
other_memory_resource mem_res_other;
auto my_other_vector = std::pmr::vector<int>(0, &mes_res_other);

auto vec = my_vector; // type is std::pmr::vector<int>
vec = my_other_vector; // this is ok -
// my_vector and my_other_vector have same type

Reference

原理

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