0%

與其說這是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 ;
    });
}

這裡的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