礦坑系列 ── std::function
礦坑系列 ── std::function
礦坑系列首頁:首頁
hackmd 版首頁:首頁
前言
std::function
是 C++11 時加入的東西,它基本上是一個類型模板(Class Template),目的是對可呼叫物件進行包裝,用起來會像是函式指標那樣,但用途更為廣泛,只要是可使用複製建構的可呼叫物件都可以使用,像是函式、lambda、std::bind
、Functor(function object) 等等。
包裝起來的可呼叫物件我們稱它為 target
,如果一個 std::function
還沒包裝任何物件,也就是沒有 target,我們稱它為空(empty),此時我們如果使用了 target,它會丟出一個 std::bad_function_call 的例外處理。
std::function
在 C++17 時有做了一些修正,遺棄了一些東西,這篇文章會以新版的規則來記錄。
為什麼要有 std::function?
也許有人想問為什麼需要有 std::function
? 直接用 function pointer 之類的不行嗎? 其實 std::function
的設計初衷就是保證泛用性,看一下這個例子:
1 |
|
上面這個例子中我們有一個 normal_func
,作用是將傳入的數字和 b = 10 印出來。然後有一個 call_func
,作用是吃一個 function pointer,並且把傳入的數字傳進這個 function pointer。
上面這個例子會正常的輸出,但如果我們今天加入了 lambda:
1 |
|
下面這兩個 lambda_func
和 lambda2
就無法傳入 call_func,因為 capture list 有東西的 lambda 是無法轉換成 function pointer 的。
此時我們就需要 std::function
了,前面有提到,這個東西的產生就是為了保證泛用性,讓 lambda 或其他 collable 物件都可以被傳入:
1 |
|
這樣一來就可以成功的運作了,可呼叫的 Class 也可以用:
1 |
|
記得要引入標頭檔 <functional>
。
語法與相關函式
這次試著改了文章的順序,先教大家怎麼用,再去講內部實現的概念,主要是因為有些人比較在意該怎麼用與何時使用,並不是太在意裡面怎麼實作的。再來雖然核心概念差不多,但不同編譯器實作的方法還是會稍微不太一樣。
語法與重要定義
語法其實挺簡單的,基本上就是
std::function<回傳型態(參數列)> 實例名 = 初始化器
初始化器就是拿來初始化的東西,前面也提到了只要是可呼叫的物件都可以拿來用,看看 cppreference 上的例子:
1 |
|
從上例可見可儲存的東西挺多的,只要是可呼叫物件基本上都可以儲存。不過不知道大家有沒有覺得有其中一個很奇怪,那就是 std::function<int( const Foo & )> f_num = &Foo::num_;
,右邊的這個 &Foo::num_
是個資料成員指標,那為什麼這個可以拿來初始化 std::function
呢?
讓我們回顧一下最一開始的定義:
std::funtion 這個類別模板提供了多型的 wrapper,通常拿來包函式指標。 這個 Wrapper 可以儲存,複製,和透過
()
呼叫任何的可呼叫物件,並允許 function 為一級函式。
接下來就有個問題了,到底什麼是可呼叫物件呢? 定義如下:
一個可呼叫物件指的是一個物件擁有可呼叫的型態 ( 20.14.3-4 )
所以接下來的問題就變成「什麼是可呼叫的型態」了,定義如下:
可呼叫型態指的是 function object type 或是一個資料成員的指標 ( 20.14.3-3 )
這樣答案就很明顯了,&Foo::num_
是個資料成員指標,代表他是可呼叫的物件,因此可以拿來初始化 std::function
。
至於 function object type 指的則是一種在 function call 內可以是後序表達式(postfix-expression) 的物件型態,至於什麼是後序表達式這邊就先不介紹了,再寫下去會有點偏題。
那麼既然知道了什麼是 function object type,那麼 function object 很直覺的就是那些型態是 function object type 的物件了。
簡單來說,可呼叫物件(collable object) 的意思就是某個有明確定義動作的物件,對 function object 來說就是透過 ()
來呼叫的相對應的物件,對函式指標來說就是呼叫相對應的函式,對成員指標來說就是拜訪相對應的成員。
複雜的定義大概就這樣,那麼 std::function
的使用時機在前言的地方也提到了,std::function
主要就是為了泛用性,消除型態上面的差異而出現的東西。 代價也不大,std::function
並不肥,能使用的話就盡量使用吧。
相關函式
在 C++17、C++20 時有部分的相關函式被刪除了,那些被刪除的函式本篇就不介紹了,主要介紹那些仍可使用的,有興趣的可以到 cppreference 上看。
這邊的內容除了怎麼使用以外可能也會扯到內部的結構,
接下來的 fn
代表某個 std::function
的 instance,簡單來說就是 std::function<void()> fn = 某個 callable object
這樣,只是一個代稱方便大家理解。
建構子
基本上內部寫好的建構子有五個,加上最新的 Defect Report(C++23) 內提出的方法有六個
1、2:
1
2function() noexcept; // 1
function( std::nullptr_t ) noexcept; // 2上面這兩個(1、2) 會建構出空的
std::function
3、4:
1
2function( const function& other ); // 3
function( function&& other ) noexcept; // 4上面這兩個(3、4) 會將 other 的 target 複製(3) 或移動(4) 到
*this
。 簡單來說就是把對象包裝起來的東西移動或複製到自己身上。如果 other 是空的,*this
也會是空的。另外,移動(4) 後的 other 會處於有效但未指定的狀態。
5:
1
2template< class F >
function( F f ); // 5前面的 4 個都是用
std::function
來初始化,這個可以說是最常用的了,這個建構子也是std::function
可以做到高泛用性的原因。基本上就是用我們給的東西來初始化內部的 target,如果想看內部的實作可以直接往下拉到內部概念那邊看,因為這扯到整個結構的問題。
不過要注意的是如果
std::function
的回傳型態是個繫結到某個沒有 trailing-return-type 的 lambda 所回傳的 reference type,由於 auto deduction 的關係,這種 lambda 回傳的型態一定會是個 prvalue,因此std::function
的回傳型態的 reference 會繫結到一個暫時物件,但這個暫時物件的生命週期會在std::function::operator()
回傳時結束,因此會變一個懸掛的 reference(dangling reference)。例子:
1
2std::function<const int&()> F([]{ return 42; });
int x = F(); // Undefined behavior: the result of F() is a dangling reference6:
1
2template< class F >
function( F&& f ); // 6最後這個是最近更新的,還不在標準裡面,但有在 C++23 的草案裡面,如果想看舊版的規範可以到這裡看,另外裡面還有對編譯器實作的建議、Postconditions 和 Preconditions 等等,有興趣的也可以進來看。
具體上的原因可以到 Defect Report(C++23) 看,簡單來說就是
operator=
吃的是F&& f
,而如果 constructor 用複製的會有一些 Value Categories 上的影響。在新版的實作內,我們會利用
std::forward<F>(f)
來初始化 target,target 的型態會是std::decay<F>::type
。如果
f
是個 null 的 function pointer、member pointer 或一個由std::function
特化的空值,那麼*this
會是一個空的std::function
。這個 constructor 不會參加多載解析(Overload resolution),除非 target 的 type 不是
std::function
,而且對於那些參數的型態 Args… 來說,它的 lvalue 是可呼叫的而且回傳的型態 R 也是可呼叫的型態。如果 target 的 type 不能複製,或者初始化的格式不對,那麼這是個 ill-formed。
解構子
destructor 就相對沒那麼複雜了,但要注意一下解構的時候的時候會連 target 一起解構。
operator=
基本上內部寫好的有五個
1:
1
function& operator= ( const function& other );
上面這個會 copy other 的 target 到
*this
的 target。2:
1
function& operator=( function&& other );
上面這個會移動 other 的 target 到
*this
的 target,此時 other 會處於未定義的有效狀態。3:
1
function& operator=( std::nullptr_t ) noexcept;
上面這個會把目前的 target 解構,讓
*this
回復為空。4:
1
2template< class F >
function& operator=( F&& f );上面這個將會把
*this
的 target 設為可呼叫的 f。除非 f 對於參數的類型 Args… 和回傳的型態 R 是可呼叫的,不然這個運算符不會執行 Overload Resolution。
5:
1
2template< class F >
function& operator=( std::reference_wrapper<F> f ) noexcept;上面這個會把 f 的 target 複製給
*this
的 target。operator bool
std::function
的 operator bool 會檢查*this
的 target 是否為空,如果非空就回傳 true,如果是空的就回傳 false。例子:
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
void sampleFunction() {
}
void checkFunc( std::function<void()> &func ) {
// 利用 operator bool 來確認 std::function 是否為空。
if ( func ) {
std::cout << "Function is not empty!\n";
}
else {
std::cout << "Function is empty. Nothing to do.\n";
}
}
int main() {
std::function<void()> f1;
std::function<void()> f2( sampleFunction );
std::cout << "f1: ";
checkFunc( f1 ); // std::function 為空,進入 else
std::cout << "f2: ";
checkFunc( f2 ); // std::function 非空,進入 if
}operator()
R operator()( Args… args ) const;
std::function
的 operator() 會傳入參數,呼叫儲存的 callable object。例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void call( std::function<int()> f ) // std::function 可以 passed by value
{
std::cout << f() << '\n';
}
int normal_function() {
return 42;
}
int main() {
int n = 1;
std::function<int()> f = [&n]() { return n; };
call( f );
n = 2;
call( f );
f = normal_function;
call( f );
}swap
語法:fn.swap(目標)
作用: 交換所存的 callable object
例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void fn1() {
std::cout << "fn 1\n";
}
void fn2() {
std::cout << "fn 2\n";
}
int main() {
std::function<void()> s1{ fn1 };
std::function<void()> s2{ fn2 };
s1(), s2(); // fn1 \n fn2
puts( "" ), s1.swap( s2 );
s1(), s2(); // fn2 \n fn1
return 0;
}target_type
const std::type_info& target_type() const noexcept;
回傳儲存的 target 的
typeid
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int f( int a ) { return -a; }
void g( double ) {}
int main() {
// fn1 and fn2 have the same type, but their targets do not
std::function<int( int )> fn1( f ),
fn2( []( int a ) { return -a; } );
std::cout << fn1.target_type().name() << '\n' // PFiiE
<< fn2.target_type().name() << '\n'; // Z4mainEUliE_
// since C++17 deduction guides (CTAD) can avail
std::cout << std::function{ g }.target_type().name() << '\n'; // PFvdE
}target
1
2
3
4
5template< class T >
T* target() noexcept; // (1)
//----------------------------------//
template< class T >
const T* target() const noexcept // (2)如果
target_type() == typeid(T)
,回傳一個指向儲存的 target 的 pointer,否則回傳一個 null 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int f( int, int ) {
puts( "calling f" );
return 1;
}
int g( int, int ) {
puts( "calling g" );
return 2;
}
void sampleFunction() {
puts( "calling ssample Function" );
}
void test( const std::function<int( int, int )> &arg ) {
std::cout << "test function: ";
if ( arg.target<std::plus<int>>() )
std::cout << "it is plus\n";
if ( arg.target<std::minus<int>>() )
std::cout << "it is minus\n";
int ( *const *ptr )( int, int ) = arg.target<int ( * )( int, int )>();
if ( ptr && *ptr == f ) {
std::cout << "it is the function f\n";
( *ptr )( 1, 1 );
}
if ( ptr && *ptr == g ) {
std::cout << "it is the function g\n";
( *ptr )( 1, 1 );
}
}
int main() {
test( std::function<int( int, int )>( std::plus<int>() ) );
test( std::function<int( int, int )>( std::minus<int>() ) );
test( std::function<int( int, int )>( f ) );
test( std::function<int( int, int )>( g ) );
std::function<void()> fn = sampleFunction;
void ( **fn_ptr )() = fn.target<void ( * )()>();
( *fn_ptr )();
}
Deduction Guides 模板推導指引
在 C++17 時有個叫 CTAD 的東西出現了,不知道的朋友可以先到 cppreference 上看,或看一下 TSJW的文章。
簡單來說就是讓模板自己推定型態的技術,舉個例子:std::vector<int> vec = {5};
可以寫成 std::vector vec = {5};
這樣,int 可以由編譯器推導出來。雖然看起來好像很簡單,但後面牽扯到的東西挺多的,像是自訂的型態等等,這邊不討論太多,因為不是今天的主題
那麼 std::function
自然也會有這個技術了,畢竟也是一個通過模板來實現的東西。
內部有兩個定義:
1 | template<class R, class... ArgTypes> |
上面這個讓 std::function
可以自動推斷出一般函式的型態。
1 | template<class F> |
而第二個這個,只有當 &F::operator()
不是拿來當計算的操作數,語法合法,且 decltype(&F::operator())
的形式是 R(G::*)(A...)
(可有 cv限定詞、noexcept、lvalue reference),用於某些 class type G 時,會進入 Overload Resolution。
看起來很複雜,但簡單來說就是 decltype(&F::operator())
是有效的,此時的型態會被推導為 std::function<R(A...)>
。
例子:
1 |
|
不過未來這個 Deduction Guides 可能還會改變,尤其是在 std::function
支援 noexcept
之後。
內部概念
內部概念我大概會講一下架構,然後帶大家看一下 GCC 上的優化與 code。
接下來的內容主要參考了 Raymond 的兩篇文章、 Stackoverflow 上的講解與 jerrt_fuyi 的文章:連結 1、連結 2、連結 3、連結 4,有興趣的可以進去看看原文。
架構
我們的目的是消除型態,雖然有很多不同的方法能達到這個目的,但概念上的架構上主要會有一個可呼叫物件的基類,之後利用模板,由這個基類衍生出不同的可呼叫物件的實例,之後 std::function
再去存取這個實例的指標,大概會像這樣:
1 |
|
這只是一個簡單的偽代碼,可能無法編譯,callable_base
提供了解構、呼叫、複製等動作的介面,再由各種可呼叫物件自行去實作。
在上面使用了 unique_ptr
,這會讓複製建構子有些難做,因為 unique_ptr
無法複製,因此在實作上通常會使用原生的指標,並自行管理資源的分配,比較複雜,但也比較方便我們優化。
可以看看另一個人幫忙做的簡化版本,這是 StackOverflow 上有人做的,他簡化的是 Ubuntu 14.04 gcc 4.8 上的 std::function
,這邊他選擇將 callable_base
那些繼承的東西給簡化掉了,比較注重在內部消除型態的實現上,我幫忙整理了一下並加上了一些註解:
1 |
|
雖然因為簡化的關係和 gcc 裡面寫的已經有點不太一樣了,但主要的核心概念仍一樣,就是透過模板來消除型態。
內部實作與優化
前言
接下來的內容有很多地方都來自 jerry_fuyi 的文章,基本上是引用過來,改寫一些部分,自己讀了很久,希望多加一些解釋,抓一些重點出來後,大家能更容易理解,如果看不習慣我的也很建議回去讀讀原文,因為真的寫得蠻好蠻詳細的。
而如果你已經看得很累了,那麼可以去休息一下,因為接下來的內容會比較生硬 XD
看完了前面約略的概念後各位可能覺得這個東西就這樣了,其實沒那麼簡單,還有很多隱藏的問題(雖然前面可能先講了答案XD,但大家可能沒想過會有這些問題),例如,函式不是一個固定大小的對象,但 class 是,我們用 class 來包裝不同的函式,卻要有相同的大小,不會有問題嗎? 還有其他很多的小問題,導致這個東西一直到 C++23 都還拿出來被討論,前面的建構子就是其中一項,有些東西可能會牽扯到 ABI Break,蠻複雜的。
實作前的想法
std::function
能儲存不同的可呼叫物件,表示有多態性,這我們前面使用了 virtual function 與繼承解決了,這是個簡單、直覺且有效的方法,但這個方法會用到動態內存,很多情況下這是種浪費,像是一個沒捕捉東西的 lambda,這種情況下他只有一個 char 的大小,但我們卻為了它去呼叫了分配動態內存的函式。
於是我們就需要想辦法避免這種浪費,剛剛說了 std::function
的大小是固定的,但是這個大小是可以讓我們自己定的,所以我們可以在 std::function
裡面放一個空白且大小適中的 field 來存這種小對象,來避免呼叫到動態內存。
而對於更大的可呼叫物件,雖然放不進這個 field,但可以放這個可呼叫物件的指標,這種 TDM 的結構可以用 union 來做。
這種小物件直接儲存,大對象才放在 heap 上並儲存指標的方法叫做 small object optimization。
在利用了繼承的實作中,可呼叫物件被包裝在一個子類裡面,std::function
會持有一個其父類的指標,但為了效率,我們需要把剛剛那個空白的 field 與這個指針用 union 包裝起來。
既然用了 union,在不確定這個 union 存的是什麼之前,當然不能去呼叫裡面的東西,因此這樣的設計我們就不能使用繼承來去消除型態了。
於是我們需要想其他的方法來達成多態性,而這邊大家可以想一下 virtual function 是怎麼實現的,有 virtual function 的 class 裡面會有個 vptr,指向一個 vtable,這個 vtable 有很多個 slot,裡面有指向 type_info 對象的指標和函式指標。
發現了嗎? 就是函式指標,函式指標也是一個能幫助我們實現多態性的東西,如果你是想在 C 裡面實現多態,比較方便的方法也是在 struct 內直接放 function pointer,因此我們主要會使用 function pointer 來實作。
開始實作
基本上 std::function
是個模板類,模板參數就是一個 type,注意是一個 type,不是好幾種 type,以這個例子來說:
1 | std::function<int(double)> f; |
f
是一個可以呼叫的對象,參數吃一個 double
,回傳一個 int
。 你可能會覺得奇怪,這邊這樣不就有兩個 type 了嗎? 怎麼還說是一個 type,其實 int(double)
自己就是一個 type,它是一種 function type,但要注意不是 function pointer。
接下來我會帶大家看一下 gcc 的實作 (待補,被高微殘害中QQ)
參考資料
1. std::function (cppreference) (文章部分來源)
5. How is std::function implemented?
6. Inside std::function, part 1: The basic idea
7. Function passed as template argument
8. What is std::function in C++, and why we need them?
9. Should I use std::function or a function pointer in C++?
10. C++ 11 std::function和std::bind使用詳解
11. What are preconditions and postconditions?
13. Class template argument deduction (CTAD)
14. 潮.C++17 | Class Template Argument Deduction 和 Deduction Guide 類別樣版參數推導
15. Difference between Undefined Behavior and Ill-formed, no diagnostic message required
17. C++11 Type Deduction With std::function
18. template argument type deduction from std::function return type with lambda
19. Argument type auto deduction and anonymous lambda functions
20. Type deduction for std::function
21. 剖析STD::FUNCTION接口与实现 (文章部分來源)
22. checking invariants in C++
23. Example of C++ class invariant