本文為C++ Weekly Ep 332的note
這次我們要來看C++下三種傳遞function的方法: 1. function pointer 2. lambda 3. std::function
function pointer
從原古C語言時代就存在的傳遞手段,基本上就是把function所在的address存起來,要呼叫的時候直接跳過去,我們來看個code:
1 | // Type your code here, or load an example. |
上面的程式展示了最基本的function pointer使用方法,我們再進一步來看出來的asm長怎樣:
1 | square(int): # @square(int) |
上面的程式我為了方便說明原理,使用-O0編譯,我們不難看出function pointer的行為其實很簡單:
- 把square()的address放進rax
- 再放進[rbp - 16]
- call [rbp - 16]
Cool, 基本上這就是最簡單的型態了,我們接著再來看看lambda會讓問題變複雜多少
lambda
lambda是C++11引入的新功能,我們可以把它想像成一種匿名函數(Anonymous Function),而且最酷的是lambda可以做到capture,不過我們先來看一個最基礎的lambda:
1 | int main() { |
不過想要搞清楚到底發生了什麼,我們可以先從cpp insight的輸出開始看起:
1 | int main() { |
我們可以發現一件很酷的事情,lambda實際上是被轉換成一個class,我們去call一個lambda實際上是兩個步驟:
- 先宣告一個lambda物件func
- 去呼叫func的method "operator()"
OK, 那實際上出來的asm會長怎樣?
1 | main: # @main |
看起來與function pointer版本差不多,但這是因為我們沒有使用到capture的原因,那用了會怎樣?
我們來稍微修改一下lambda的內容
然後再看看出來的asm長怎樣:1
2
3
4
5
6
7
8
9
10int main() {
int num = 12 ;
auto func = [&]() {
num += 1 ;
return num*num;
} ;
func() ;
return num ;
} // main()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
29main: # @main
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 0
mov dword ptr [rbp - 8], 12
lea rax, [rbp - 8]
mov qword ptr [rbp - 16], rax
lea rdi, [rbp - 16]
call main::$_0::operator()() const
mov eax, dword ptr [rbp - 8]
add rsp, 16
pop rbp
ret
main::$_0::operator()() const: # @"main::$_0::operator()() const"
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
mov rcx, qword ptr [rbp - 8]
mov rax, qword ptr [rcx]
mov edx, dword ptr [rax]
add edx, 1
mov dword ptr [rax], edx
mov rax, qword ptr [rcx]
mov eax, dword ptr [rax]
mov rcx, qword ptr [rcx]
imul eax, dword ptr [rcx]
pop rbp
ret仔細看(8.)的mov,之所以會多出這一道指令是因為capture的實現方式是這樣的,我們來看cpp insight:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int main() {
int num = 12;
class __lambda_3_17 {
public:
inline /*constexpr */ int operator()() const {
return num * num;
}
private:
int & num; // << HERE!
public:
__lambda_3_17(int & _num)
: num{_num} // AND HERE!
{}
};
__lambda_3_17 func = __lambda_3_17{num};
func.operator()();
return 0;
} // main()原來capture是透過class member variable實現的,所以才會多出一個mov指令
另外,一個沒有capture的lambda是可以被隱式轉型成function pointer的,見以下範例:
1 | int main() { |
std::function
std::function與上面提到的兩個東西有一個根本上的不同,std::function是一個function wrapper,實現原理可參見A Simplified std::function Implementation,這裡要提的是std::function可以包裝上面的兩個東西之外,任何具有operator()的牛鬼蛇神都可以包,為程式開發提供了極大的彈性
But, 如果我們沒有給std::function綁定一個callable就直接call他會怎樣?
1
2
3
4
5
6
7
8
9
10
11
12
int main() {
std::function<int (int)> func;
auto numnum = [](int num) {
num += 1 ;
return num*num;
} ;
return func(1) ;
} // main()執行結果是直接拋出例外:
- terminate called after throwing an instance of 'std::bad_function_call' what(): bad_function_call
為了提供開發彈性,std::function背地裡做了很多......非常多的努力
- 確保你提供的callable的return type與std::function預期的相同
- 確保你提供的callable不會遇到生命週期的問題
- 根據實作的不同,有可能是把你的callable直接複製一份到heap上......
- 可能比較厲害一點的實作會動用到SOO(small object optimization)
那到底用不用?
- 有需要就用,好比說......
- 你在寫一個遊戲引擎,你需要用std::function來存你的script engine產生出的function(lua:叫我嗎?)
- 我要塞function進去container(vector, stack, std::array.....)
- 各種彈性 > 效能的場景
- 有需要就用,好比說......
延伸閱讀
First-class function What is the performance overhead of std::function?