0%

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)

從one_of說起

之前寫過這樣的 Code Snippet

1
2
3
4
5
6
7
8
9
10
#include <initializer_list>
enum state_type { IDLE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED };
template <typename ...Ts>
bool is_any_of(state_type s, const Ts& ...ts)
{
bool match = false;
(void)std::initializer_list<int>{(match |= (s == ts), 0)...};
return match;
}
is_any_of(s, IDLE, DISCONNECTING);

這程式碼適用於C++11,如果用C++17可以用fold expression更進一步簡化

1
2
3
4
5
6
enum state_type { IDLE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED };
template <typename ...Ts>
bool is_any_of(state_type s, const Ts& ...ts)
{
return ((s == ts) || ...);
}

能不能更進一步?

variadic non type template

1
2
3
4
5
6
7
enum state_type { IDLE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED };
template <state_type ... states>
bool is_any_of(state_type s)
{
return ((s == states) || ...);
}
is_any_of<IDLE, DISCONNECTING>(s);

可以看出,這個版本將比較值跟貝比教值分開,比原先的版本更容易看出語意,不過問題也很明顯,所以被比較值要在編譯期就決定了,靈活度反而比不上原先的方案,有沒有折衷的方案?

std::tuple

看到Modern Techniques for Keeping Your Code DRY這方案真是眼睛一亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename ...Ts>
struct any_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return std::apply([&t](const auto& ...ts) { return ((ts == t) | ...); },
static_cast<const std::tuple<Ts...>&>(*this));
}
template <typename T>
friend bool operator==(const T& lh, const any_of& rh) { return rh == lh; }
};

template <typename ...Ts>
any_of(Ts...)->any_of<Ts...>;
any_of(IDLE, DISCONNECTING) == s
s == any_of(IDLE, DISCONNECTING)

用CTAD,std::tuple,std::apply的組合技包裝成威力十足的武器

Add more operation

假設我們現在要加上<的比較

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename ...Ts>
struct any_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return std::apply([&t](const auto& ...ts) { return ((ts == t) || ...); },
static_cast<const std::tuple<Ts...>&>(*this));
}

template <typename T>
bool operator<(const T& t) const {
return std::apply([&t](const auto& ...ts) { return ((ts < t) || ...); },
static_cast<const std::tuple<Ts...>&>(*this));
}
};

可以看出兩段程式碼大同小異,能否亙進一步?

Higher order functions

由於兩個operator都是由一系列or operation組成的
因此我們可以把or operation提出來

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename F, typename ...Ts>
bool or_elements(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) || ...); }, t);
}

template <typename ...Ts>
struct any_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return or_elements([&t](const auto& v) { return v == t; }, *this);
}

template <typename T>
bool operator<(const T& t) const {
return or_elements([&t](const auto& v) { return v < t; }, *this);
}
};

template <typename ...Ts>
any_of(Ts...)->any_of<Ts...>;

至此any_of的東西就差不多了,那麼來寫all_of吧

all_of implementation

大同小異

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename F, typename ...Ts>
bool and_elements(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) && ...); }, t);
}

template <typename ...Ts>
struct all_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return and_elements([&t](const auto& v) { return v == t; }, *this);
}

template <typename T>
bool operator<(const T& t) const {
return and_elements([&t](const auto& v) { return v < t; }, *this);
}
};

template <typename ...Ts>
all_of(Ts...)->all_of<Ts...>;

一般來說做到此就行了,能否亙進一步?

Inheritance

再上去就是炫技了,實用度就不高了,編譯時間又拉長不少

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
struct or_elements {
template <typename F, typename ...Ts>
static bool apply(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) || ...); }, t);
}
};

struct and_elements {
template <typename F, typename ...Ts>
static bool apply(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) && ...); }, t);
}
};

template <typename Op, typename ...Ts>
struct op_t : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return Op::apply([&t](const auto& v) { return v == t; }, *this);
}

template <typename T>
bool operator<(const T& t) const {
return Op::apply([&t](const auto& v) { return v < t; }, *this);
}

template <typename T>
friend bool operator==(const T& lh, const op_t& rh) { return rh == lh; }
template <typename T>
friend bool operator>(const T& lh, const op_t& rh) { return rh < lh; }
};

template <typename ...Ts>
struct any_of : op_t<or_elements, Ts...> {
using op_t<or_elements, Ts...>::op_t;
};

template <typename ...Ts>
struct all_of : op_t<and_elements, Ts...> {
using op_t<and_elements, Ts...>::op_t;
};

template <typename ...Ts>
any_of(Ts...)->any_of<Ts...>;
template <typename ...Ts>
all_of(Ts...)->all_of<Ts...>;

Reference

Modern Techniques for Keeping Your Code DRY
Lambdas: From C++11 to C++20, Part 1
Lambdas: From C++11 to C++20, Part 2

從C# Extension Method說起

1
2
3
4
5
6
7
8
9
static class StringUtilities
{
public static int WordCount(this string text)
{
return text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
var text = "This is an example";
var count = text.WordCount();

將一個Method綁在已有的type之上
在C++有類似的提案,叫做UFCS
不過提案會不會過還不知道,但是可以用目前的技術模擬
以下從Reddit看來的兩種方式,寫下自己的看法

Ver 1

使用CRTP技巧來達成

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
// One way to emulate UFCS in standard C++.

// A CRTP class that forwards .call<F>(...) to F::function(*this, ...)
template<typename T>
struct uniform_call_syntax {
template <typename F, typename... Args>
constexpr auto call(Args... args) {
return F::function(static_cast<T&>(*this), args...);
}
};

// Usage:
struct my_type : public uniform_call_syntax<my_type> {
int value;
};

struct set_value {
static my_type& function(my_type& o, int i) {
o.value = i;
return o;
}
};

struct add_value {
static my_type& function(my_type& o, int i) {
o.value += i;
return o;
}
};

struct get_value {
static int function(const my_type& o) {
return o.value;
}
};

int test() {
my_type obj;

return obj
.call<set_value>(10)
.call<add_value>(20)
.call<get_value>(); // returns 30.

}

作法顯而易見,每個需要UFCS功能的都需要繼承uniform_call_syntax,然後每個Externsion method都需要有同樣的function name
缺點也顯而易見,要有這功能就需要繼承,有些type是不允許修改的

Ver 2

版本二需要C++14以上的標準

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
#include<string>
#include<iostream>

// Same as Haskell's infix operator `&` or F#'s operator `|>`
template<typename Arg, typename F>
decltype(auto) operator % (Arg&& arg, F&& fn)
{
return std::forward<F>(fn)(std::forward<Arg>(arg));
}

namespace my
{
auto tolower = []
{
return [](auto&& s) -> decltype(auto)
{
for (auto& c : s) {
c = ('A' <= c && c <= 'Z') ? c - ('Z' - 'z') : c;
}
return decltype(s)(s);
};
};

auto append(const std::string& what)
{
return [&](auto&& s) -> decltype(auto)
{
s += what;
std::cerr << "is-lvalue-reference:" << std::is_lvalue_reference<decltype(s)>::value << ";" << s << "\n";
return std::forward<decltype(s)>(s);
};
};
}

int main()
{
// as rvalue-references
auto s1 = std::string{ "HELLO," }
% my::tolower()
% my::append(" world");

// as lvalue-references
auto s2 = std::string{ "GOODOBYE," };
s2% my::tolower()
% my::append(" cruel world.");

std::cerr << s1 << "\n" << s2 << "\n";

return 0;
}

看起來很美,很像Functional Programming的Pipe觀念
不過對Programmer的要求比較高,至少要有FP的基本觀念才比較容易理解

看了C++17的文章之後,還是搞不太清楚兩者的差異,於是自己寫程式驗證一下

Capture this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <memory>
struct Obj {
Obj(int value) : v(std::make_unique<int>(value)) {}
~Obj() { printf("Obj have been destroyed\n"); }
Obj(const Obj& o) {
printf("Copy constructor invoked\n");
v = std::make_unique<int>(*o.v);
}
Obj(Obj&& o) {
printf("Move constructor invoke\n");
v = std::move(o.v);
}
auto test() { return [this] { printf("%d\n", *v); }; }
std::unique_ptr<int> v;

};
int main()
{
auto test = Obj(123).test();
test();
}

很不意外的,Crash掉了
如果我們把呼叫改成Obj(123).test()(),Obj的Lifetime就跟執行時間一樣長了,可以正常執行

Capture *this

1
2
3
struct Obj {
auto test() { return [*this] { printf("%d\n", *v); }; }
};

跟上面一樣,只是修改了capture this的方式

1
2
3
4
Copy constructor invoked
Obj have been destroyed
123
Obj have been destroyed

由此可見他會呼叫Copy construtor,然後摧毀了原先的Obj,lambda中的this指的就是Copy的Obj

Simulate *this without C++17

要這麼做也不是不行

1
2
3
struct Obj {
auto test() { return [self = *this] { printf("%d\n", *self.v); }; }
};

C++17的程式碼比較乾淨,不過這個版本就很清楚知道這個this是複製品了