Microsoft Visual C++コンパイラにおける、C++ラムダ式から関数ポインタへの変換と関数呼び出し規約(calling convention)*1の扱いについてメモ。
MSVC11以降では変数キャプチャを伴わないラムダ式(stateless lambda)を、任意の呼び出し規約をもつ関数ポインタ型へと変換できる*2。MSDNより言及箇所を引用(下線部は強調)。
Lambdas
http://msdn.microsoft.com/en-us/library/hh567368%28v=vs.110%29.aspx
(snip) Additionally in Visual C++ in Visual Studio 2012, stateless lambdas are convertible to function pointers. (snip) (The Visual C++ in Visual Studio 2012 is even better than that, because we've made stateless lambdas convertible to function pointers that have arbitrary calling conventions. This is important when you are using APIs that expect things like__stdcall
function pointers.)
注意:本記事の内容はVisual Studio 2012(MSVC11)コンパイラ出力結果からの推測に基づく。
MSVC11コンパイラへの入出力
#include <cstdio> int main() { auto lm = [](){ std::puts("hello"); }; // OK: ラムダ式のオブジェクトを直接呼び出し lm(); // OK: stdcall関数ポインタ経由で呼び出し void (__stdcall * fp1)() = lm; fp1(); // OK: cdecl関数ポインタ経由で呼び出し void (__cdecl * fp2)() = lm; fp2(); }
上記C++ソースコードをコンパイルすると、下記のアセンブリコードが出力される。(Debugビルド、/RTC無効、/FAs出力より実処理コードのみ抽出・整形)
EXTRN __imp__puts:PROC str0 DB 'hello', 00H <lambda>::operator() PROC ; 4 : auto lm = [](){ std::puts("hello"); }; push OFFSET str0 call DWORD PTR __imp__puts ret <lambda>::operator() ENDP <lambda>::operator void (__fastcall*)(void) PROC mov eax, OFFSET <lambda>::<helper_func_fastcall> ret <lambda>::operator void (__fastcall*)(void) ENDP <lambda>::<helper_func_fastcall> PROC call <lambda>::operator() ret <lambda>::<helper_func_fastcall> ENDP <lambda>::operator void (__stdcall*)(void) PROC mov eax, OFFSET <lambda>::<helper_func_stdcall> ret <lambda>::operator void (__stdcall*)(void) ENDP <lambda>::<helper_func_stdcall> PROC call <lambda>::operator() ret <lambda>::<helper_func_stdcall> ENDP <lambda>::operator void (__cdecl*)(void) PROC mov eax, OFFSET <lambda>::<helper_func_cdecl> ret <lambda>::operator void (__cdecl*)(void) ENDP <lambda>::<helper_func_cdecl> PROC call <lambda>::operator() ret <lambda>::<helper_func_cdecl> ENDP _main PROC ; 3 : { ; 4 : auto lm = [](){ std::puts("hello"); }; ; 5 : lm(); lea ecx, DWORD PTR -1$[ebp] call <lambda>::operator() ; 6 : void (__stdcall * fp1)() = lm; lea ecx, DWORD PTR -1$[ebp] call <lambda>::operator void (__stdcall*)(void) mov DWORD PTR -8$[ebp], eax ; 7 : fp1(); call DWORD PTR -8$[ebp] ; 8 : void (__cdecl * fp2)() = lm; lea ecx, DWORD PTR -1$[ebp] call <lambda>::operator void (__cdecl*)(void) mov DWORD PTR -12$[ebp], eax ; 9 : fp2(); call DWORD PTR -12$[ebp] ; 10 : } xor eax, eax ret _main ENDP
推測される内部処理
出力アセンブリコードより、MSVC内部ではラムダ式から下記擬似C++コードへ変換していると推測される。
- ラムダ式の本体は
lambda::operator()
が対応。__thiscall
呼び出し規約(ecxレジスタ=thisポインタ)。 - ラムダ式の型
lambda
は、各呼び出し規約の関数ポインタ型へのユーザ定義変換演算子を提供する。 - ユーザ定義変換演算子は、ラムダ式本体へと転送するヘルパ
lambda::helper_func_XXX()
への関数ポインタを返す。
// 元ソースコードのラムダ式 [](){ std::puts("hello"); } // 等価な擬似C++コード struct lambda { // ラムダ式 本体 void __thiscall operator()(void) { std::puts("hello"); } // fastcallヘルパ関数 static void __fastcall helper_func_fastcall(void) { // self = this self->operator()(); } // fastcall関数ポインタ型へのユーザ定義変換 typedef void (__fastcall * fp_fastcall)(void); operator fp_fastcall() { return &lambda::helper_func_fastcall; } // stdcallヘルパ関数 static void __stdcall helper_func_stdcall(void) { // self = this self->operator()(); } // stdcall関数ポインタ型へのユーザ定義変換 typedef void (__stdcall * fp_stdcall)(void); operator fp_stdcall() { return &lambda::helper_func_stdcall; } // cdeclヘルパ関数 static void __cdecl helper_func_cdecl(void) { // self = this self->operator()(); } // cdecl関数ポインタ型へのユーザ定義変換 typedef void (__cdecl * fp_cdecl)(void); operator fp_cdecl() { return &lambda::helper_func_cdecl; } };
メモ:この結果は最適化なしのアセンブリ出力から推測したものであり、Releaseビルドではヘルパ関数→ラムダ式本体のような転送処理はインライン展開され得る(実際に展開される)。
関連URL