0%

Asynchronous programming

Why asynchronous programming

Asynchronous programming 是個反人類的思考的東西,就算選擇不同的程式語言,共識最好的Network programming model,都是這個樣子,一個connection一個thread

1
2
3
4
5
6
7
8
9
10
11
listen(socket_fd, 20);

/* Looooop */
while (1) {
newsocket_fd = accept(socket_fd,
(struct sockaddr *) &client_addr,
&client_len);
pthread_t thread;
pthread_create(&thread, NULL, run_thread, (void *) newsocket_fd);
pthread_join(thread, NULL);
}

這個Model可以解決95%的問題,不過人生最難的就是那個But,這個Programming Model不能Scale

C10K Problem (1999)

這就是著名的C10K Problem,是Operation System的問題,OS不能有跟Connection一樣多的Thread,就算可以,也會耗費大量的Memory,以及頻繁的Context Switch
山不轉路轉,於是出現了IO multiplexing技術,也就是大家熟知的select/poll/epoll

The early stage of asynchronous programming

一開始的asynchronous programming,就算是libuv,asio或是nodejs等,都需要一個callback當參數,寫著寫著就會變成這樣

The problem of callback

  • 反人類

Thread based solution之所以被推崇,就是人類的思考模式傾向於直線思考,而Callback based solution需要將步驟切得七零八落,慘不忍睹

  • 難寫易錯

假設事務夠簡單,一兩層callback就能解決的話,事情還好辦,當邏輯複雜到一個程度,寫錯的機率實在是太高了

Source Code是要寫給人看的,因此需要有工具來管理複雜度,也就是Coroutine

System Language對於Coroutine的態度

  • C:不關我的事,你自己想辦法
  • C++: 到了2021年還沒有標準的Network Library:會不會太落後
  • Rust: 比C++早訂定標準:不過押寶押錯了:標準也定了:改不了了:至於押寶押錯這件事後面再說

What is coroutine

太陽底下沒有新鮮事,Coroutine在1963年就被提出,過了五十年後重新被人想起
Coroutine擁有以下四種特性

  • Invoke
  • Return
  • Yield
  • Resume

而我們一般所知道的Function就只有

  • Invoke
  • Return

也就是Function只是Coroutine的特殊案例

Coroutine的另一項特性

  • Cooperative multitasking

同樣的,太陽底下沒有新鮮事
聽過當初Windows 3.1常常會程式卡死,而Windows 95不會,就是因為將Cooperative multitasking改成Pre-emptive multitasking

The simplest example on coroutine

雖然這範例沒什麼用,不過能夠讓我們了解Corotuine的本質,能夠Yield和Resume
switch的case可以包含在for loop迴圈裡面,不過蔗不是本文重點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int counter(void) {
static int i, state = 0;
switch (state) {
case 0: /* start of function */
for (i = 0; i < 10; i++) {
state = 1; /* so we will come back to "case 1" */
return i;
case 1:; /* resume control straight after the return */
}
}
}
int main()
{
for (int i = 0; i < 10; i++)
printf("%d\n", counter());
}

上面這個只是個玩具Coroutine,真正能拿來用的還分幾類
至於怎麼做就各顯神通了

Two difference model on Coroutine

就算是Coroutine,也可以分成兩類

  • Stackful Coroutine
  • Stackless Coroutine
    顧名思義,差異就在對Stack的處理上面
  • Stackless將State放在Heap上,而Stackful放在Stack上
  • Stackless的大小是動態分配的,Stackful的Stack是固定大小的
  • Stackless本質是個StateMachine,而Stackful是個User Mode Thread
    因此Stackess Machine的Runtime消耗比較小,Stackful相反
  • Stackful可以和舊有的synchronous code組合,Stackless不行
  • Stackless需要Compilier支援,Stackful只需要Library就能做了
  • Stackless的方案有傳染性,例如你在Javascrupt所看到的
    1
    2
    3
    async func1() {
    await func2();
    }
    你的async/await是成雙成對的,布這麼用就會出錯,而Stackful沒有此限制
  • Stackful的程式好寫,Stackless需要一定能力

選邊站

由於兩種Model差異很大,由於程式語言的特性以及歷史因素,不同程式語言的選擇也不一樣

  • Stackless:C#(第一個使用async/await的主流語言),Javascript,Python,C++,Rust,Kotlin(雖然是JVM的語言,不過跟Java選擇不同)
  • Stackful:Golang(其實是變種的Coroutine),Java(照抄Golang那套,不過還沒推出),PHP(in the future)

Goroutine

前面提到,Goroutine是Stackful Coroutine的變形,最主要的差異在於

  • coroutine是順序執行
  • Goroutine可以在多個cpu平行執行的
    因此又產生了分歧點
    假設我們有Coroutine A,B,C
    C等待B的資料,B等待A的資料
  • 如果是傳統的Coroutine,A執行完會transfer到B,B執行完會transfer到C,由於在同一個CPU上,資料不用加鎖
  • 如果是Goroutine,A,B,C三者可能在不同的CPU上跑,關於資料的傳遞只能透過Channel
  • 由於Golang實作了一個有效利用Cpu Usage的Runtime,將corotuine定義成light weight thread,所以Golang Runtime需要做一部分OS需要做的事情,例如Schedule coroutine
  • Mandatory goroutine,就算你寫一個hello world也避不掉
  • Goroutine不快,Maximum network connection也比不上Stackless Coroutine(C++/Rust)
  • 不過程式好寫太多,這強項才是goroutine搶走PHP/Python的主要原因

押錯寶

講講Rust押寶押錯的故事

IO Model有兩種

如同Coroutine有兩種,IO EventLoop也有兩種

  • Proactor:最著名的就是Windows的IOCP了
  • Reactor:select/poll/epoll等都是
    Rust使用epoll的Reactor Model,不過epoll不是linux的未來

CPU Spectre and Meltdown

就跟COVID-19一樣,Spectre和Meltdown改變了寫程式的方向
因為CPU的Bug,Linux修正方向,io_uring才是Linux的未來,而io_uring和IOCP一樣,是Proactor的model

Influence

由於標準定了,要改改不了了
如果要改的話只有兩種選擇

  • 重新制定標準,然後變成v2版本,光是制定一個版本花了四年,這次應該會快一點
  • 兩個Model是可以互轉的,只是會有Performance Loss,當Spectre和Meltdown的Patch打上去之後會掉多少更難以估計

Conclusion

  • 如果你是那95%的人,根本用不上Asynchronous programming,直接使用thread model,還不容易錯
  • 如果不幸是那5%的人,首先考慮golang,golang就算幾千個缺點,goroutine都能掩蓋過去
    golang適合寫網路服務,也只能寫網路服務
  • 如果你是一秒鐘幾千萬上下,出來跑得遲早都要還,逃不掉C/C++/Rust寫code了
    這裡有個實際案例
    Why Discord is switching from Go to Rust
  • 沒有最好的方案,只有適合的方案

Coroutine的文章太難寫了,只好先寫篇簡單的
這是一篇工程性的文章,給對這方面有興趣的人

Motivation

由於看了某AI部門寫的C++服务编译耗时优化原理及实践這篇文章,想要分享一下思路 ,不過編譯二十分鐘就在哀哀叫 (當初我前公司動輒一兩個小時

Clarification

  • 這問題跟C++無關,這是C語言的問題
  • C++的編譯模型跟C語言一樣(Before C++20)
  • 不過由於C++有template和header only libraries而將這問題放大了很多倍
  • 不信的話可以試試看編譯Linux kernel

Root Cause

哪有什麼Root Cause,這不是個Bug

只是跟不上時代

C語言誕生在1969-1973,至今超過五十年了
當初計算機能力比不上現代,因此產生了
Header / Implementation Separation的做法
C++繼承了這個Compiling Model

Modern Language怎麼做

將宣告和實做擺在一起

  • Application Language: Java/C#/Javascript/Golang
  • System Language: Zig/Rust

About Future

分兩方面來說

C

C語言已經是個Inactive的語言了

  • 自從C11之後,不加入重大新功能,只做相容性改善
  • 別期待它會加入Module功能

C++

相較於C,C++從11之後努力追趕Modern Language的路線

  • 直到C++20之後,才推出正式的Module Spec
    • 不過何時可以用上未可知
    • Cloud端也許明年就能使用,embedded等vendor改朝換代不知道要多久
    • 就算全面普及之後,ecosystem也要一段時間才能趕上
    • 以Javascript為例,從commonJS切到Module也走了五六年

What can we do now

未來會怎麼發展不知道,不過現在有幾個選項

什麼都不要做

這不是Bug,不管它也無妨,如果真在意編譯速度的話,當初我前公司買Incredibuild做分散式編譯
根據最前面那篇文章的數據,使用分散式編譯效果比其他方法都有用

Precompiled Headers

將常用的header通通include在一起,然後編譯這個大的header file
有興趣可以參考Using pre-compiled headers in GCC/Clang using CMake and usage in Catch2

Reduce Header dependency

這可以分成幾方面來討論

Find unused header

舉個例子

1
2
#include <stdio.h>
int add(int a, int b) { return a + b; }

stdio.h在這邊就是完全沒必要的,如果是goalng,import fmt然後沒用到根本編譯不過,不過golang這作法是個雙面刃,我不喜歡

如果要工具的話可以考慮[include-what-you-use](

Choose suitable third party libraries

不可能所有東西都自己寫,當你需要某個功能的時候,先找找是否有適合的選項,問題是如果選擇太多了該怎麼辦,總不能

文中討論到Boost,Boost最大的問題是它依賴性太重了
當你需要一台腳踏車的廠警,選擇一台坦克車絕對不是個好主意

Opaque Pointer

俗稱編譯防火牆的技術,是當的斷開header dependency
以C語言來說,header大概長這樣

1
2
3
4
5
struct obj;

size_t obj_size(void);
void obj_setid(struct obj *, int);
int obj_getid(struct obj *);

implementation是這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "obj.h"

struct obj {
int id;
};

size_t obj_size(void) {
return sizeof(struct obj);
}

void obj_setid(struct obj *o, int i) {
o->id = i;
}

int obj_getid(struct obj *o) {return o->id; }

C++版的叫pImpl,如果有看過我的程式碼應該不陌生,掠過

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>

class PublicClass {
public:
PublicClass(); // Constructor
PublicClass(const PublicClass&); // Copy constructor
PublicClass(PublicClass&&); // Move constructor
PublicClass& operator=(const PublicClass&); // Copy assignment operator
PublicClass& operator=(PublicClass&&); // Move assignment operator
~PublicClass(); // Destructor

// Other operations...

private:
struct CheshireCat; // Not defined here
std::unique_ptr<CheshireCat> d_ptr_; // Opaque pointer
};

Conclusion

雖然這不是個Bug,不過有人題就表示這是個需求,未來會怎麼走沒人知道,只能現有的材料下能做些什麼改進

看到很多文章,總覺得從co_await開始講解實在很難清楚表達
寫了自己的版本當作筆記

The simplest coroutine

這程式碼什麼都沒幹,不過就是一個最小的coroutine了
一個coroutine至少要有co_returnco_awaitco_yield其中之一

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
using namespace std;
#if defined(__clang__)
#include <experimental/coroutine>
using namespace std::experimental;
#else
#include <coroutine>
#endif

struct Task {
struct promise_type {
auto initial_suspend() { return suspend_never{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return Task(coroutine_handle<promise_type>::from_promise(*this)); }
void return_void() {}
void unhandled_exception() {}
};
Task(coroutine_handle<promise_type> h) : handle(h) {}
~Task() {
if (handle)
handle.destroy();
}
coroutine_handle<promise_type> handle;
};

Task coroutineDemo()
{
co_return;
}

int main() {
auto task = coroutineDemo();
return 0;
}

Under the hood

Compilier做了很多事情
像這樣的Psuedo Code

1
2
3
4
5
template <typename TRet, typename … TArgs>
TRet func(TArgs args…)
{
body;
}

被Compilier處理之後大概變成這個樣子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename TRet, typename ... TArgs>
TRet func(TArgs args...)
{
using promise_t = typename coroutine_traits<TRet, TArgs...>::promise_type;

promise_t promise;
auto __return__ = promise.get_return_object();

co_await promise.initial_suspend();

try
{ // co_return expr; => promise.return_value(expr); goto final_suspend;
body; // co_return; => promise.return_void(); goto final_suspend;
} // co_yield expr; => co_await promise.yield_value(expr);
catch (...)
{
promise.unhandled_exception();
}

final_suspend:
co_await promise.final_suspend();
}

先忽略co_await的語句,之後補上
因此我們可以看到prmise_type裡面有initial_suspendfinal_suspend等function,這個promise_type是定義coroutine的行為模式
之後會對promise_type做更進一步的說明,這邊就此打住

How the compiler chooses the promise type

看到上面的Pseudo code

1
using promise_t = typename coroutine_traits<TRet, TArgs...>::promise_type;

然後看看coroutine_traits的定義

1
2
3
4
5
6
7
8
9
10
template <class _Ret, class = void>
struct _Coroutine_traits {};

template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {
using promise_type = typename _Ret::promise_type;
};

template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};

就是看TRet裡面有沒有promise_type的struct definition了
者李又分成兩類

直接定義在class裡面

就是我們範例那個做法,簡單直接

將promise_type抽出

當你有一群Coroutine,然後這群Coroutine雖然有些許不同,但是對Coroutine的控制流程相同,就可以用這方案

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
struct Promise {
auto initial_suspend() { return suspend_never{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return T(coroutine_handle<Promise>::from_promise(*this)); }
void return_void() {}
void unhandled_exception() {}
};

struct Task {
// Ignore
using promise_type = Promise<Task>;
};

著名的C++ coroutine都使用此方式

coroutine_handle

看到我們的Task當中有coroutine_handle了嗎

1
2
3
struct Task {
coroutine_handle<promise_type> handle;
};

這個才是coroutine的本體,負責讓暫停的coroutine繼續執行,或是判斷coroutine是否執行完畢
將原先的範例加強一下,來示範coroutine_handle該怎麼使用

A failure example

原本我想寫個像這樣的程式加深對coroutine_handle的使用

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
#include <iostream>
using namespace std;
#if defined(__clang__)
#include <experimental/coroutine>
using namespace std::experimental;
#else
#include <coroutine>
#endif

struct Task {
struct promise_type {
auto initial_suspend() { return suspend_always{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return Task{ coroutine_handle<promise_type>::from_promise(*this) }; }
void return_void() {}
void unhandled_exception() {}
};
coroutine_handle<promise_type> handle;
~Task() {
if (handle)
handle.destroy();
}
void resume() { handle.resume(); }
bool done() const { return handle.done(); }
};

Task coroutineDemo(int times)
{
for (size_t i = 0; i < times; i++) {
cout << "coroutineDemo\n";
co_await suspend_always{};
}
co_return;
}

int main() {
auto task = coroutineDemo(3);
while (!task.done()) {
task.resume();
}
std::cout << "Done\n";
return 0;
}

結果不如預期,發現問題出在

Once execution propagates outside of the coroutine body then the coroutine frame is destroyed. Destroying the coroutine frame involves a number of steps:

  1. Call the destructor of the promise object.
  2. Call the destructors of the function parameter copies.
  3. Call operator delete to free the memory used by the coroutine frame (optional)
  4. Transfer execution back to the caller/resumer.

問題就出在3這步

  • Visual C++不會在coroutine body結束時立刻delete coroutine frame
  • GCC和Clang會,當coroutine frame destroy之後,呼叫handle.done()handle.resume()是use-after-free

Solution

研究了一下, 只要將final_suspend改成

1
2
3
4
struct promise_type {
// ignored
auto final_suspend() noexcept { return suspend_always{}; }
};

讓它在coroutine結束之前停下來就可以了

Reference

Function template overload

拿Boost.Serialization當範例

1
2
3
4
5
6
7
namespace boost::serialization {
template <class Archive>
void serialize(Archive& ar, two_things& tt, unsigned int version) {
ar & tt.a;
ar & tt.b;
}
}

Pros:

  • 就像膠水,將黏貼部分獨立成單一Unit,需要的時候再引入即可

Cons:

  • 跟前個方式依樣,需要打開library的namespace
  • 跟前一個方式依樣,存在Name Collisions的可能

Friend member function

又是Boost.Serialization

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <boost/serialization/serialization.hpp>
struct two_things {
private:
friend class boost::serialization::access;
template <class Archive>
void serialize(Archive& ar, unsigned int version) {
ar & a;
ar & b;
}
public:
int a;
bool b;
};

優缺點正好跟上面相反

Pros:

  • 不需要打開library的namespace
  • 不會造成Name Collisions

Cons:

  • Tightly coupled with structure

當我們如果不需要serialization功能時,透過上面的方式,我們只需要不include implemtation unit即可,不過這方法就不行了
另外一點,這方式引入了boost::serialization的依賴,不管我們需不需要,都必須承受這副作用

ARGUMENT-DEPENDENT LOOKUP

C++最難理解的特性之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace {
struct two_things {
int a;
bool b;
};
void do_something(two_things &t) {
printf("do_something in anonymous namespace\n");
}
};

int main()
{
two_things t;
do_something(t);
}
Worng usage example of ADL (std::swap)

What’s wrong with 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
namespace A {
struct two_things {
int a;
bool b;
};
void swap(two_things &, two_things &)
{
printf("swap in namespace A\n");
}
};
namespace B {
template <typename T>
void test(T& a, T&b)
{
swap(a, b);
}
}
int main()
{
A::two_things a, b;
int c, d;
B::test(a, b);
B::test(c, d);
}
  • swap(a, b) // invokes ADL because call name is unqualified
  • std::swap(a, b) // does not invoke ADL because call name is qualified

如果我們把swap改成std::stap,自訂億的swap就毫無作用了
正確的做法應該是

1
2
3
4
5
6
7
8
namespace B {
template <typename T>
void test(T& a, T&b)
{
using std::swap;
swap(a, b);
}
}
Correct usage example of ADL (std::swap)
  • Create a callable function object which does the two-step with an internal detail namespace’s swap.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    namespace nonstd {
    namespace detail {
    template <typename T>
    void swap(T&, T&) { printf("nonstd::swap\n"); }
    struct swap_func {
    template <typename T>
    void operator()(T &a, T&b) const noexcept {
    swap(a, b);
    }
    };
    }
    inline constexpr const auto swap = detail::swap_func{};
    }

    namespace B {
    template <typename T>
    void test(T& a, T&b)
    {
    nonstd::swap(a, b);
    }
    }
    這做法在Reference中的文章有特別說明,被稱作Customization Point Object
    亦或是niebloid(以作者的名稱命名的單字)
Friend or not?

假設我們把 namespace A中的swap從free_function變成friend function

1
2
3
4
5
6
7
8
struct two_things {
int a;
bool b;
friend void swap(two_things &, two_things &)
{
printf("swap in namespace A\n");
}
};

兩者是等價的,不過

  • friend function 可以存取 structure的private field,free function不能
  • friend function只能被ADL找到
Downgrade to C++11

inline variable是C++17才有的玩意 在有些時候用不了新標準的情況之下 有必要找個方法向下相容

1
2
3
4
5
6
7
8
namespace nonstd {
// older standards:
template <typename T>
struct __static_const { static constexpr T value{}; };
template <typename T>
constexpr T __static_const<T>::value;
constexpr const auto& swap = __static_const<detail::swap_func>::value;
}

Reference

看到了Customization Point Objecttag_invoke之後,想要把
Static Polymorphism in C++這個主題總結一下,而這是第一部分

What is Static Polymorphism

基本上可以這樣定義

  • Static polymorphism with overloaded functions and templates that happens at compile time;
  • Dynamic polymorphism with interfaces that happens in run-time.
    而Static的好處在於Compile time就能知道該呼叫哪個函數,沒了Virtual table和runtime的開銷,有機會inline optimization

The simplest form - Class Template

最著名的粒子大概就是std::hash了

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Foo {};

namespace std {
template <>
struct hash<Foo>
{
size_t operator()(const Foo& x) const
{
/* your code here */
}
};
}
std::unordered_map<Foo, int> map;

大概就是這個樣子

  1. 定義自己的類型
  2. 打開Library的namespace,填入需要的運重是馬
    至於優缺點嗎
    優點:
  • 只有一個參數,和一個functor operator,理解起來不困難
    缺點:
  • 需要汙染Library的namespace
  • 無法2對Library的default type做override
1
2
3
4
5
6
7
8
9
10
11
namespace std {
template <>
struct hash<double>
{
size_t operator()(const int x) const
{
/* your code here */
}
};
}
std::unordered_map<double, int> map;

這樣就直接死給你看

  • 要包含整個definition,不能只包含forward declaration

Extened Class Template - Class selection

先定義Primary template definition

1
2
3
4
5
6
namespace test {
template <typename T, typename C = void>
struct get {
int operator()() { return 42; }
};
}

FULL SPECIALIZATION

跟上面的範例差不多

1
2
3
4
5
6
namespace test {
template <>
struct get<double> {
int operator()() { return 66; }
};
}

PARTIAL SPECIALIZATION

1
2
3
4
5
6
namespace test {
template <typename T>
struct get<T, std::enable_if_t<std::is_arithmetic_v<T>>> {
int operator()() { return 11; }
};
}

SPECIALIZATION COLLISIONS

一旦我們加入了一組SPECIALIZATION

1
2
3
4
5
6
namespace test {
template <typename T>
struct get<T, std::enable_if_t<std::is_integral_v<T>>> {
int operator()() { return 33; }
};
}

就爆炸了,由於有兩組class符合條件,不知道選哪個好
比起單參數的方案,變得更複雜,更難維護
之前單參數的缺點它都有,還加上一連串的問題
增加的問題比解決的問題還多,不建議使用

Reference

C++Now 2019: JeanHeyd Meneide “The Plan for Tomorrow: Extension Points in C++ Applications”

Why Concepts?

實例分析

Function overload by type

當我們需要一個函數toString,傳入一個參數,Pseudo Code大概是這樣

1
2
3
4
5
6
7
8
9
template <typename T>
std::string toString(const T& input)
{
if (input have toString member function) {
return input.toString();
} else {
return "unknown format";
}
}

該怎麼做

C++98 Way

看看就好, 這手法現在也不需要理解了, 太舊了

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
template <typename T>
class has_toString {
private:
typedef char Yes;
typedef Yes No[2];

template <typename U, U> struct really_has;

template <typename C> static Yes& Test(really_has <std::string(C::*)() const,
&C::toString>*);

template <typename> static No& Test(...);

public:
static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.

template<class T> // A specialisation used if the expression is true.
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.

template <typename T>
std::string toString(const T& input, typename enable_if<has_toString<T>::value, int>::type t = 0)
{
return input.toString();
}

template <typename T>
std::string toString(const T& input, typename enable_if<!has_toString<T>::value, int>::type t = 0)
{
return "unknown format";
}

我想你不會懷念他的
幾個缺點
– 沒有type_traits,所以enable_if之類的要自己寫
– 每增加一個Signature偵測,就要多個類似has_toString的東西出現
– Function overload的signagure要修改
– 奇醜無比

C++11 Way

僅列出跟C++98不同的地方

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
template <typename T>
class has_toString {
private:
typedef char Yes;
typedef Yes No[2];

template<typename C> static auto Test(void*)
-> decltype(std::string{ std::declval<C const>().toString() }, Yes{});

template<typename> static No& Test(...);

public:
static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template <typename T, typename std::enable_if<has_toString<T>::value, int>::type t = 0>
std::string toString(const T& input)
{
return input.toString();
}

template <typename T, typename std::enable_if<!has_toString<T>::value, int>::type t = 0>
std::string toString(const T& input)
{
return "unknown format";
}

基本上差不了多少,不過現在不用修改toString的Signature,順眼多了
C++14之後的更新基本上不可用,MSVC不支援,就不列出了

Boost Hana Solution

1
2
3
4
5
6
7
8
9
10
auto has_toString = boost::hana::is_valid([](auto&& obj) -> decltype(obj.toString()) { });

template <typename T>
std::string toString(T const& obj)
{
return boost::hana::if_(has_toString(obj),
[] (auto& x) { return x.toString(); },
[] (auto& x) { return "unknown format"; }
)(obj);
}

這基本上就很接近我們的Pseudo code,不過還要額外的Hana dependency

C++17

C++14之後的更新基本上不可用,MSVC不支援,就不列出了
不過我們可以用if constexpr把兩個 toString合併成一個

Think more

我們只是檢查一個條件toString,如果有復合的條件約束的話該怎麼辦, 假設我們還要檢查一個ver的member variable或者這個typeu定義了一個value_type,上面的方式就變得無力

C++ 20 Concept Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
concept has_toString = requires(const T &t) {
typename T::value_type;
{ t.ver } -> int;
{ t.toString() }->std::same_as<std::string>;
};

template <typename T>
std::string toString(const T& input)
{
if constexpr (has_toString<T>)
return input.toString();
else
return "unknown format";
}

配合C++17的if constexpr,很漂亮的解決了上述的難題

Debug Template Code

在寫Generic template code的時候,往往要跟Error message打交道
有了Concept約束之後,比較容易觀測出到底哪裡錯了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
concept have_value = requires(T a) {
a.value;
};

template<typename T>
requires have_value<T>
auto get_value(const T& a)
{
auto v = a.value;
// do some thing
return v;
}

int main()
{
int b = 10;
auto y = get_value(b);
}

錯誤訊息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<source>:2:1: error: 'concept' does not name a type
2 | concept have_value = requires(T a) {
| ^~~~~~~

<source>:2:1: note: 'concept' only available with '-fconcepts'
<source>:7:1: error: 'requires' does not name a type
7 | requires have_value<T>
| ^~~~~~~~

<source>: In function 'int main()':
<source>:18:12: error: 'get_value' was not declared in this scope
18 | auto y = get_value(b);
| ^~~~~~~~~

Compiler returned: 1

很明顯的我們的Type不符合Concept的要求
這個範例簡單到就算用原先的方式,有經驗的Porgrammer一樣能夠找出錯誤
如果你的Code是一層一層的Template累加起來,就知道這東西多有用了

How to use Concept

Four Ways

Requires Clause

1
2
3
template <typename T>
requires std::integral<T>
void log(T&& x);

Trailing Requires Clause

1
2
template <typename T>
void log(T&& x) requires std::integral<T>

雖然比起第一種方式顯得冗長,不過有種特異功能第一種方式做不到

1
2
template <typename T>
void log(T&& x) requires std::integral<decltype(x)>

由於Trailing Requires Clause可以看到整個Function signature, 因此可以使用decltype做處理

Constrained Template Parameters

1
2
template <std::integral T>
void log(T&& x)

很顯然的, T這個類型必須滿足std::integral的約束
這方案還有一種好處,在不支援C++20的Compilier,只要把std::integral換成typename
就能照常編譯了,不過享受不到Concept的好處
不過可以在C++20檢查過之後Poring回Pre-C++20的Compiler

Abbreviated function template

顧名思義,這種方式只能在 Function template上用,是一種語法糖
看看以下這個案例

1
2
3
4
Arithmetic auto sub(Arithmetic auto fir, Arithmetic auto sec) 
{
return fir - sec;
}

基本上等價於Constrained Template Parameters

1
2
3
4
5
template <Arithmetic T, Arithmetic T2>
auto sub(T fir, T2 sec)
{
return fir - sec;
}

比起上面的寫法,連template宣告都省下來了,不過上面阿寫法可以蛻化為Pre-C++20,這不行

Function overload

Concept允許function overload,比起一般的template function, 通過concept檢驗的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
void overload(auto t){
std::cout << "auto : " << t << std::endl;
}

template <typename T>
requires std::is_integral_v<T>
void overload(T t){
std::cout << "Integral : " << t << std::endl;
}

void overload(long t){
std::cout << "long : " << t << std::endl;
}

int main(){

std::cout << std::endl;

overload(3.14); // (1)
overload(2010); // (2)
overload(2020l); // (3)

std::cout << std::endl;

}

Customize Concept

The simplest concept

Concept就是Compile-time的boolean expression

1
2
template <typename T>
concept superfluous = true;

既然是個boolean expression,就能做所有boolean operator支援的操作

1
2
3
4
template <typename T>
concept integral = std::is_integral_v<T>;
template <typename T>
concept number = std::integral<T> || std::floating_point<T>;

More complext concept

定義一個能支援遞增的Concept

1
2
3
4
5
template <typename T>
concept you_can_increment_it = requires(T x)
{
{++x};
};

定義能夠進行四則運算的Concept, 注意這邊需要兩個Type

1
2
3
4
5
6
7
8
template <typename X, typename Y>
concept they_are_mathsy = requires(X x, Y y)
{
{ x * y };
{ x / y };
{ x + y };
{ x - y };
};

對Return type有要求的Concept

1
2
3
4
5
6
template <typename T>
concept you_can_increment_it = requires(T x)
{
{++x} -> std::same_as<T>;
{x + x} -> std::convertible_to<int>;
};

檢查type是否有member structure和member function

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
concept its_a_dragon = requires(T x)
{
// this type trait must be able to be instantiated
typename dragon_traits<T>;

// T has a nested typename for some pointer idk use your imagination
typename T::dragon_clan_ptr;

{x.breathe_fire()};
{x.dragon_breath_firepower()} -> std::convertible_to<uint>;
};

Concept all the thing

當然,你也可以在其他地方加上Concept

1
Integral auto integ = getIntegral(10);

你的return value自然就要符合Integral的約束,幫助我們在Code Maintenance和Refactoring中得到優勢

Conclusion

比起C++Module和C++Coroutine改變遊戲規則的重要性比起來,Conceptˋ只能算是錦上添花,不過對Library developer來說還是有用的

Reference

Is it possible to write a template to check for a function’s existence?
Notes on C++ SFINAE
An introduction to C++’s SFINAE concept: compile-time introspection of a class member
Substitution Failure is Error and Not An Error.
C++20: Concepts, the Details
C++20 Concepts
C++20: Define Concepts

雖然這東西沒什麼技術含量,不過流程也是要自己跑過一遍才會有記憶點,如果只看過沒跑過很快就忘
現代的Javascript Develop實在麻煩,需要很多工具配合使用

先決條件

沒什麼好講的

1
2
$ mkdir modern_javascript_library && cd modern_javascript_library
$ npm init -y

Typescript

身為不專業的C++開發者,我比較能接受Typescript,所以就用上了

1
$ npm install typescript --save-dev

接著編寫tsconfig.json,依需求自行修改

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
{
"compilerOptions": {
"allowUnreachableCode": true,
"strictNullChecks": true,
"strict": true,
"esModuleInterop": true,
"target": "es5",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": false,
"sourceMap": true,
"outDir": "./lib",
"lib": [
"dom",
"esnext"
],
"downlevelIteration": true,
"moduleResolution": "node",
"baseUrl": ".",
"rootDir": ".",
"paths": {
"@/*":["src/*"],
"@/types":["src/types"]
}
},
"include": [
"src/main.ts"
]
}

可以看到我們的進入點是src/main.ts
接著就開始寫src/main.ts

1
2
import print from './print'
print()

看到我們import了print,所以要補上這個檔案

1
2
3
4
5
6
function print()
{
console.log('Hello World!')
}

export default print

接著編譯它

1
$ npx tsc

驗證結果

1
2
$ node lib/src/main.js
Hello World!

第一階段完成,我們可以看到產生了main.js和print.js兩個檔案,接著我們要打包在一起了

Rollup

Rollup是個打包工具,跟Webpack比起來更適合打包Library,在這個地方我們使用它

1
$ npm install rollup @rollup/plugin-typescript --save-dev

編寫rollup.config.js

1
2
3
4
5
6
7
8
9
10
import typescript from '@rollup/plugin-typescript';

export default {
input: 'src/main.ts',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [typescript()]
};

執行它

1
$ npx rollup -c

可以看到產生了output/main.js一個檔案而已

1
2
$ node output/main.js
Hello World!

結果正確

ESLint

ESLint也是現代JS開發不可或缺的玩意,一併用上

1
$ npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev

編寫.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module" // Allows for the use of imports
},
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
}
};

修改package.json,新增lint Rule

1
2
3
"scripts": {
"lint": "tsc --noEmit && eslint 'src/*.{js,ts,tsx}' --quiet --fix"
},

接著跑一次看看

1
2
3
$ npm run lint
> modern_javascript_library@1.0.0 lint /home/hm/modern_javascript_library
> tsc --noEmit && eslint 'src/*.{js,ts,tsx}' --quiet --fix

因為程式碼不複雜,自然也不會有什麼問題產生

Prettier

對Javascript Code做Formatter的工具,類似clang-format或是gofmt,少不了又要配置一堆東西

1
$ npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev

新增.prettierrc.js

1
2
3
4
5
6
7
module.exports = {
semi: true,
trailingComma: "all",
singleQuote: true,
printWidth: 120,
tabWidth: 4
};

修改上一步驟的.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
"plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module" // Allows for the use of imports
}
};

同上一步,執行

1
$ npm run lint

如果沒有錯誤的話,ts檔案會auto formatter

Conclusion

這東西做個一次可能還很新鮮,重複幾次下來就會煩躁
因此一堆人開發了Scaffold來做這件事,畢竟我不是專業JS開發者,淺嘗則止
哪天轉職之後再來看也不遲,不過由於Javascript太弱了,才需要一堆工具,真是麻煩

契機

雖然知道Logdown是一個不再維護的產品,不過得過且過,畢竟搬家麻煩,不過當前幾天XDite跑路,Logdown打不開的時候,危機意識就產生了
總有一天這個地方會被關掉,趁著Logdown還活著的時候趕緊搬家

我的Blog寫作平台史

Wordpress

沒什麼好講的,用mysql存檔到沒什麼問題,問題在於存起來的格式,對於Programmer來說,Embedded code根本就是災難

Octopress

用Markdown寫Blog,當初也花了很多時間在建立環境,重點是維護環境很麻煩,Deploy也麻煩,更慘的是,Octopress停止開發,
Ruby我也看不太懂

Logdown

這是我用最久的組合,只負責寫作,Depoly就交給後台負責了,不過負責人跑了,不跟著跑才有鬼

Hexo

雖然有許多類似的玩意 (Hugo/Vuepress/Gatsby) ,不過我還是選擇最簡單的方案,我不是個合格的Frontned Developer,我只想寫作
加上Github action的功能,我終於可以把Deploy這件事交給別人做了

2020新的開始,就想辦法克服懶病,多寫一些廢文吧

Reference

通过 Github Actions 自动发布 Hexo 博客

由於是個Kubernetes初學者,一切從頭開始
基本上是趙個這篇Write a Kubernetes-ready service from zero step-by-step開始的,加上一些新玩意
– 把dep換成Golang原生的Module
– 利用 Docker Multistage Build,可以參考透過 Multi-Stage Builds 改善持續交付流程
– 將裡面的Minikube換成了microk8s
– 原本想自己架Private Docker Registry,K8S那邊搞不定,放棄,乖乖用Docker Hub

搞定環境的時間比寫程式的時間多十倍

成果就放在go-k8s-example

由於是k8s新手,所以希望先從Minikube這樣的小東西開始學習
microk8s現在只有Debian/Linux有,window或是MacOS還是乖乖用Minikube吧
首先先安裝snapd

1
$ sudo apt install -y snapd

接著透過snapd安裝 microk8s

1
$ sudo snap install microk8s --classic

將自己加入microk8s的group

1
$ sudo usermod -a -G microk8s `whoami`

登出Session之後重登入

1
2
$ type microk8s.kubectl
microk8s.kubectl is hashed (/snap/bin/microk8s.kubectl)

由於microk8s的指令都是microk8s開頭的,我們希望跟kubectl一樣

1
2
3
$ sudo snap alias microk8s.kubectl kubectl
$ type kubectl
kubectl is hashed (/snap/bin/kubectl)