0%

C resource managment, defer, unique_ptr, out_ptr, template auto and coroutine

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