yohhoyの日記

技術的メモをしていきたい日記

ラムダ式から呼び出し規約指定つき関数ポインタへ

Microsoft Visual C++コンパイラにおける、C++ラムダ式から関数ポインタへの変換と関数呼び出し規約(calling convention)*1の扱いについてメモ。

MSVC11以降では変数キャプチャを伴わないラムダ式(stateless lambda)を、任意の呼び出し規約をもつ関数ポインタ型へと変換できる*2MSDNより言及箇所を引用(下線部は強調)。

Lambdas
(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.)

http://msdn.microsoft.com/en-us/library/hh567368%28v=vs.110%29.aspx

注意:本記事の内容は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

*1:x86アーキテクチャを前提とする。wikipedia:en:X86_calling_conventions参照

*2:“変数キャプチャを伴わないラムダ式から関数ポインタへの変換”はC++11標準規格で定義される仕様。本記事では変換先関数ポインタの「呼び出し規約」に着目している。なおC++標準規格では呼び出し規約について何ら定義しない。