出去玩了一趟,好久沒寫一些東西,不然都要乾涸了
這觀念也很簡單,假設我們有類似這樣的程式碼
1 | template <typename T> |
這段程式在C++20是編譯不過,可是C++23放鬆了限制,允許這種寫法
不過根據神人的解法,在C++20可以模擬這種動作
1 | template <class... T> |
雖然繞了一點,但是能用
出去玩了一趟,好久沒寫一些東西,不然都要乾涸了
這觀念也很簡單,假設我們有類似這樣的程式碼
1 | template <typename T> |
這段程式在C++20是編譯不過,可是C++23放鬆了限制,允許這種寫法
不過根據神人的解法,在C++20可以模擬這種動作
1 | template <class... T> |
雖然繞了一點,但是能用
Type Punning是指用不同類型的Pointer,指向同一塊Memory address的行為,這是Undefined beahvior,可能會造成未知的錯誤.
例如
1 | #include <iostream> |
Type punning違反了Strict aliasing rule
寫網路程式的時候常常會遇到這種情形,分配一塊記憶體,然後Cast成另外一種Type的Pointer填值
1 | typedef struct Msg |
C語言的話可以使用union
1 | union { |
或是使用(unisnged / signed) char *
取代上面的int*
可以認為j從char*
轉匯成type *
是合法的,反之不成立
1 | int x = 42; |
這樣是合法的,不過缺點就是要多一次拷貝
C++20引進的新東西,不過實作也就只是上面的memcpy包裝
1 | template <class To, class From> |
C++23引進的新觀念,類似於reinterpret_cast,不過沒有undefined behaviro的副作用
1 | struct ProtocolHeader { |
一開始看到GAT也不知道在幹嘛,是看到Could someone explain the GATs like I was 5?才有感覺]
最簡單的範例,現在有一個struct
1 | struct Foo { bar: Rc<String>, } |
假設你要同時支援 Rc
和’Arc
的版本
該怎麼做
1 | struct FooRc { bar: Rc<String>, } |
不過這當然沒什麼好說的
理論上辦得到,不過沒什麼優點
我希望能寫成這樣
1 | struct Foo<P: Pointer> { bar: P<String>, } |
這樣是編譯不會過的,有了GAT之後,可以寫成這樣
1 | trait PointerFamily { type Pointer<T>; } |
不過用C++對我來說反而更好理解,就是用nested template來做
首先是等價的版本
1 | template <typename T> |
不過對於這問題,還有更簡單的方法
用template template parameter即可
1 | template <typename T> |
1 | trait Mappable { |
等價的C++版本大概是
1 | template <class T> |
這裡的Resouce不光指Memory,可能是FILE,或是ffmpeg那種Handle
Resource Management一直都是個討論的重點,要混合在C++使用,有很多種方法
拿FILE來舉例好了
1 | FILE *fp = fopen(...); |
這種方法最直接,不用學其他額外的方法,不過常常會因為程式碼的改變,而忘記release resource這件事,因此才有其他流派生存的機會
大概的程式碼長這樣,不過在C++不一定叫defer,可能叫ScopeGuard之類的東西,不過原理是一樣的
1 | FILE *fp = fopen(...); |
在小規模的使用是沒問題的,當Resoruce 一多就會變得冗餘,例如
1 | FILE *fp1 = fopen(...); |
於是C++ RAII的方式出現了,有鑑於shared_ptr
耗費較多的資源,這邊的方案都是unique_ptr
為主
為每個resource寫出一個Wrapper
1 | struct FILEWrapper { |
沒什麼不好,只是工作量太大,每加一種Resource就要有個Wrapper,那有沒有其他方案
同樣以FILE舉例,新增一個function object
1 | #include <stdio.h> |
這樣看起來跟上面差不了多少
另一種方法是
1 | std::unique_ptr<FILE, int(*)(FILE *)> fp(fp, fclose); |
這種方式比上面那個還差
雖然跟上面無關,不過這也是unique_ptr的一部分,一併提出
由於API設計的關係,input需要的是double pointer
程式有些可能會變成這樣
1 | std::unique_ptr<ITEMIDLIST_ABSOLUTE, CoTaskMemFreeDeleter> pidl; ITEMIDLIST_ABSOLUTE* rawPidl; |
這時候就是out_ptr使用場警
1 | std::unique_ptr<ITEMIDLIST_ABSOLUTE, CoTaskMemFreeDeleter> pidl; |
雖然這是在C++23才進入標準庫,不過
GitHub - soasis/out_ptr: Repository for a C++11 implementation of std::out_ptr (p1132), as a standalone library!
已經可以先嘗鮮了
C++17之後,放寬template的要求
於是這樣的程式碼成為可能
1 | template <auto destroy> |
配合上C++20的Concept之後,成為威力強大的武器
以下是從Meeting CPP 2022中節錄出來的片段
1 | template <typename T, auto * ConstructFunction, auto * DestructFunction> |
幾乎修正了上面所說的痛點
使用上也只要
1 | c_resource<FILE, fopen, fclose> fp; |
算是目前看到最通用的解法
這算是另闢新徑的方案,RAII的方案都把release resource放在destructor中
自從C++20引進Corotuine,產生了新的可能
使用上大概會是這樣
1 | co_resource<FILE*> usage() { |
滿足C++11中對Alloocator的需求,所能寫出的最簡單allocator
注意
1 | #include <cstdlib> |
而要用自己的Allocate就可以這麼做
1 | std::vector<int, Minallocator<int>> v; |
不常用,有用到再說
已知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 | template <typename _Tp1> |
這樣的程式碼會有問題
1 | ector<int, Minallocator<int>> pool_vec { 1, 2, 3, 4 }; |
因為兩者的Allocator Type不同,所以直接複製不行,所以只要兩者相同就行了,也就是C++17 PMR的初衷
新提出來的memory_resource
是個asbtract class,不同的instance會有不同的行為
因此可以可以這樣做
1 | // define allocation behaviour via a custom "memory_resource" |
1 | #include <iostream> |
透過上面的index_addr
,name_addr
,fun_print_addr
等,可以對object進行操作
而反射主要分成兩部分
雖然目前的官方標準還沒出來,不過現在有兩大流派
什麼辦不到的事情,用Marco就好了
以Boost Describe舉例
1 | struct X |
其他Macro Based的方案也差不多,就是另外定義一個Macro,自動生成類似上面的Metadata
不過這邊的問題就是
另外一派就是借助libclang來動手生成,透過Parse C++ AST來生成需要的API
舉例說明
1 | class MyClass |
生成的Metadata可以這麼使用
1 | reflang::Class<MyClass> metadata; |
這個方案的問題在於
雖然現有的Reflection library多的跟山一樣,不過眾口難調,有些是針對特定用途設計的,無法涵蓋其他方面的使用,有些功能完整,但是難用
於是乎就有人想要對語法方面下手,成為C++ Standard中的一部分
1 | template <class T> |
reflexpr和decltype一樣是type-based,所以可以套用到type based metaprogramming中
不過會不會成為標準是另外一回事了
跟Network Library一樣,成為標準之前先用成熟的方案解決
How to write comparsion operator for custom type
假設我們有一個類別
1 | struct Value { |
我們要怎麼寫出的程式碼
1 | Value v1, v2; |
有幾種方式
一種是當member function存在
手動寫出所有comparsion operator
1 | struct Value { |
另外一種是Free function存在
1 | bool operator<(const Value &lhs, const Value &rhs) { return lhs.v < rhs.v; } |
兩種實現原理相同,看情況選擇要用哪種,現在要討論的是其他的問題
當我們需要支持更多運算符號時,我們就需要寫更多的Function
1 | bool operator>(const Value &lhs, const Value &rhs); |
如果我們需要支援另外一種Type
1 | struct Value1 { |
然後又要出現一堆複製貼上加上手動修改的產物
1 | bool operator<(const Value1 &lhs, const Value1 &rhs); |
寫起來麻煩又沒什麼技術含量
有些operator可以用其他operator表示,例如Not Equal就是Not + Equal
所以我們可以用CRTP技巧減少我們的程式碼
1 | template<class Derived> |
其他的operator可以如法炮製,很多的C++ Graphics/Math Library都用了這個技巧
只要實作<
和==
,可以用來推導出其他四種比較關係
不過很不直觀,CRTP就是一種Hack,那有沒有更好的方法
Spaceship oerator也叫做The Three-Way Comparison Operator
這是C++20的一個特性,直接上Code來說明
1 | #include <compare> |
而Compiler直接為你生成Comparsion Code,原先的程式碼視為這樣
1 | (a <=> b) < 0 //true if a < b |
這種方式類似於strcmp
,會回傳<0
,>0
,0
三種情形
基本上這樣就滿足了80%的需求了,不過人生最難的就是那個But
有需要的話自定義比較方式的話,可以自定義comparsion operator
1 | struct Value1 { |
不過現在spaceship operator必須回傳的是std::strong_ordering
,std::weak_ordering
,std::partial_ordering
其中之一
至於三種ordering的差異,在此不探討,需要的話去Reference看,大部分只需要std::strong_ordering
即能完成需求
由於繼承自C語言,所以會遇到像這樣的問題
1 | // my_std.h |
來自於不同的Library,且提供不同的實作,在使用上會出現一些問題
而C語言時代的解法就是對Function Name加料
1 | // my_std.h |
而C++做的事情差不多,用namespace隔開
1 | // my_std.h |
全名是Argument-Dependent Lookup
只要有一個參數在函數的命名空間內,在使用的時候就不用加namespace prefix
在ADL只關心函數,不包含Function Object,這點在之後會用到
1 | namespace A |
如果沒有ADL,最後那行只能這樣寫了
1 | std::operator<<(std::cout, 1); |
應該沒人會喜歡
這是拓展問題的最好範例
1 | namespace std { |
如果我們要對自己的class做swap動作時,該怎麼做
1 | namespace My { |
直覺的寫法可以這樣做
1 | ```cpp |
這樣寫是Undefined Beahvior
而另外一種做法是
1 | template<> |
不過如果是My::A<T>
的話就不管用了
而比較常用的手法,就是利用ADL
1 | void fun(...); // 1 |
呼叫的foo(a)
時,會考慮2和3,1是因為在Code的namespace已經找到一個fun了,部會在往上層的scope去尋找
利用ADL two-step
的手法來拓展我們的std::swap
1 | #include <utility> |
在這個範例當中,呼叫swap的時候沒加上namespace,而讓std::swap
注入當今的Scope下,如果可以透過ADL找到對應的函數,則用特化版的函數,不然就用原先的std::swap
做預設值
最大的問題在於
1 | using std::swap; |
可能一不小心就寫成
1 | std::swap(a1, a2); |
不會報錯,頂多是效能差
另外一個比較大的問題是這個
1 | namespace __my_std_impl |
在__my_std_impl::distance(b, b)
的地方會報錯
原因在於__distance_impl
階段會進行ADL動作,在box的定義上尋找是否有__distance_impl
的函數,因找到incomplete value,故報錯
一種可能的解法就是加上namespace
1 | template<typename T> |
兩階段ADL的最大問題就是容易誤用
因此叫Standlard library來幫你做這件事
其中最簡單的CPO就長這樣
1 | namespace std::ranges { |
這裡的swap是個constexpr object,而不是個function,不過他是一個functor,因此可以適用於所有std::swap的環境下
CPO還有一個優勢,它是一個object,所以它能夠這樣用
1 | some_ranges | views::transform(ranges::begin) |
而
1 | some_ranges | views::transform(std::begin) |
這樣用不合法,因為它是個template function
Niebloids是要解決另外一個問題,去除掉不想要的ADL candicate
禁用的方法就是讓它成為CPO
以下是StackOverflow的範例
1 | #include <iostream> |
如果找到的是function object,則不會使用ADL
根據libunifex裡面的描述,一樣是透過ADL,要解決以下兩個問題
- Each one internally dispatches via ADL to a free function of the same name, which has the effect of globally reserving that identifier (within some constraints). Two independent libraries that pick the same name for an ADL customization point still risk collision.
- There is occasionally a need to write wrapper types that ought to be transparent to customization. (Type-erasing wrappers are one such example.) With C++20’s CPOs, there is no way to generically forward customizations through the transparent wrap
比較大的問題是第一點,由於透過ADL尋找函數,所以每個namespace下都需要將函數名稱當作保留字
1 | namespace std::range { |
也就是你用了swap當CPO之後,其他地方都要保留swap當作保留字不能使用,tag_invoke就是為了這點而生的
參考C++11 tag_invoke的實作 duck_invoke
1 | #include <bfg/tag_invoke.h> |
主要的作法是
formula
由於Executors跳票了,所以tag_invoke也不一定是最終解決方案
目前有其他提案,不過會不會被接受也在未定之天
詳細可以找找P2547R0來研究
如何理解 C++ 中的 定制点对象 这一概念?为什么要这样设计?
c++ execution 与 coroutine (一) : CPO与tag_invoke
C++特殊定制:揭秘cpo与tag_invoke!
Customization Points
Argument-dependent lookup - cppreference.com
Why tag_invoke is not the solution I want (brevzin.github.io)
What is a niebloid?
ADL,Concepts与扩展C++类库带来的思考
Duck Invoke — tag_invoke for C++11
寫了一堆CC++的文章,是時候換換口味了
Macro在Rust也有,不過不同於C/C++的Texture Replace
Rust的Macro強大的不得了,順便也跟C++的template做個比較
C macro版的min或是C++ templaate版的就不提供了,寫到不想寫了
直接看Rust的
1 | macro_rules! min { |
這樣看起來沒什麼特別的
那如果多加一個變數呢
1 | macro_rules! min { |
同樣的macro,可以有兩種不同的使用方式意
C語言的marco板本長這樣
1 | #define min_2(a, b) ((a) < (b)) ? (a) : (b) |
看起來就是一堆亂七八糟拼湊的組合怪
來看看Template版
1 | template <typename T> |
憑藉於Function overloading,可讀性高很多,唯一比較麻煩的是要寫兩次template function declaration
來個varadic個版本,先寫個看起來沒問題,實際上編譯不過的
1 | macro_rules! min { |
後來發現Rust Macro裡面不能有local variable,只能改成這樣
1 | macro_rules! min { |
之後又發現一點和C/C++ preprocessor不同的地方,由於他是直接對AST做操作,所以得到的Token要自己Parse
所以做個實驗,參數之間分隔用;
取代,
,這樣是合法的
1 | macro_rules! min { |
不過沒辦法用local variable有點可惜,
Marco版的,我寫不出來,直接看Variadic Template的版本
1 | template <typename T, typename... Args> |
可以做更多的變化,不過Variadic Template最大的問題是我永遠記不住...
到底要放哪這件事`
不過Rust真正厲害的是第二種Macro
基本上就是把輸入的TokenStream
轉成另外的TokenStream
的流程
分成三種
1 | $ cargo new macro-demo --lib |
在Cargo.toml
新增以下兩行
1 | [lib] |
1 | #[proc_macro_attribute] |
How to use atribute macro
1 | #[sorted] |
1 | #[proc_macro] |
How to use function-like macro
1 | seq! { n in 0..10 { |
1 | #[proc_macro_derive(Builder)] |
How to use derived macro
1 | #[derive(Builder)] |
Procedural Macros
不同於Declarative Macros
,必須單獨是一個crate存在,目前IDE對Proc Macro的支持度不好,連Debug Proc Macro也很麻煩,最常使用的還是print大法
從別人的範例中學來的,這邊實作一個Attribute macros
1 | $ mkdir rust_proc_macro_demo && cd rust_proc_macro_demo |
修改proc_macro_define_crate/Cargo.toml
加入
1 | [lib] |
接著修改rust_proc_macro_guide/Cargo.toml
1 | [dependencies] |
置換掉proc_macro_define_crate/src/lib.rs
裡面的內容
1 | use proc_macro::TokenStream; |
一樣將rust_proc_macro_guide/src/main.rs
內部的內容換掉
1 | use proc_macro_define_crate::mytest_proc_macro; |
接著用cargo check
檢查
1 | $ cd rust_proc_macro_guide/ |
可以看到類似這樣的輸出
1 | Attr TokenStream [ |
這樣我們就能看出Attr和Item分別對應的TokenStream了
有些時候,光看Lexer的TokenStream無助於解決問題,我們需要Syntax Tree
因此我們修改mytest_proc_macro
1 | use proc_macro::TokenStream; |
會跑出這樣的結果
1 | Attr [ |
要達到類似的功能,除了X-Macros之外,我想不到類似的方法了
不過X-Marcos不僅醜,功能還有限,Debug更困難
– Rust Macro 手册
– Rust宏编程新手指南【Macro】
– Rust 过程宏 101
– The Little Book of Rust Macros
– Rust Latam: procedural macros workshop
– Macros in Rust: A tutorial with examples - LogRocket Blog
– Overloading Macro on Number of Arguments
故事起源來自於看到類似這樣的程式碼
1 | #define VL_RESTORER(var) \ |
利用RAII來保存上下文當前的值,執行到結束的時候恢復原狀
不過
1 | int a = 1, b = 2; |
用起來沒什麼問題,不過總要找個題目來練習
基本上就是RAII的變形,在Destructor的部分執行我們需要的Function,隨便在github搜尋就一堆了,這邊有個最簡單的方案
1 | template <typename F> |
如果使用上C++17的CTAD,底下的make_scope_exit
也不一定得存在
所以問題就變成了這樣,我希望在結束的時候,將所存的變數恢復原狀
問題就變成了該怎麼做
雖然C++不是標準的Functional Programming Language,不過要做點手腳還是辦得到的
問題變成了,傳入需要保存狀態的變數,回傳是一個函數,執行這個函數就能恢復原狀,這裡用上了Variadic Template和Tuple
1 | template <typename ...Ts> |
這邊有兩個tuple,其中restore_ref保存了所有變數的reference,store則是變數這個時間點的值
上面的方式能夠寫成
1 | int a = 1, b = 2; |
好壞就見仁見智了