16

次のコードは、VC++ 2012 でコンパイルされました。

void f1(void (__stdcall *)())
{}

void f2(void (__cdecl *)())
{}

void __cdecl h1()
{}

void __stdcall h2()
{}

int main()
{
    f1(h1); // error C2664
    f2(h2); // error C2664

    f1([](){}); // OK
    f2([](){}); // OK

    auto fn = [](){};

    f1(fn); // OK
    f2(fn); // OK
}

エラーは正常だと思いますが、OK は異常です。

だから、私の質問は次のとおりです。

  1. C++ラムダ関数の呼び出し規約は何ですか?

  2. C++ ラムダ関数の呼び出し規約を指定するには?

  3. 呼び出し規約が定義されていない場合、ラムダ関数を呼び出した後にスタックスペースを正しくリサイクルする方法は?

  4. コンパイラはラムダ関数の複数のバージョンを自動的に生成しますか? つまり、次の疑似コードとして:

    [] __stdcall (){};

    [] __cdecl (){}; 等

4

2 に答える 2

15

VC++ 2012 では、"ステートレス ラムダを関数ポインターに" 変換するときに、コンパイラはステートレス ラムダ (キャプチャ変数を持たない) の変換を自動的に呼び出すことを選択します。

MSDN C++11 の機能:

ラムダス

[...] さらに、Visual Studio 2012 の Visual C++ では、ステートレス ラムダを関数ポインターに変換できます。[...] (Visual Studio 2012 の Visual C++ はそれよりもさらに優れています。これは、ステートレス ラムダを、任意の呼び出し規則を持つ関数ポインターに変換できるようにしたためです。これは、__stdcall関数ポインターのようなものを期待する API を使用している場合に重要です。 .)


編集:

注意:呼び出し変換は C++ 標準外です。プラットフォーム ABI (アプリケーション バイナリ インターフェイス) などの他の仕様に依存します。

次の回答は、 /FAs コンパイラ オプションを使用した出力アセンブリ コードに基づいています。単なる推測ですので、詳細については Microsoft にお問い合わせください ;P

Q1. C++ ラムダ関数の呼び出し規約は何ですか?

Q3. 呼び出し規約が定義されていない場合、ラムダ関数を呼び出した後にスタックスペースを正しくリサイクルする方法は?

まず、C++ lambda(-expression)は関数 (または関数ポインター) ではありませんoperator()。通常の関数の呼び出しのようにラムダ オブジェクトを呼び出すことができます。また、出力アセンブリ コードは、VC++ 2012 が__thiscall変換を呼び出してラムダ ボディを生成することを示しています。

Q2. C++ ラムダ関数の呼び出し規約を指定するには?

私の知る限り、方法はありません。(のみの場合もあります__thiscall

Q4. コンパイラはラムダ関数の複数のバージョンを自動的に生成しますか? すなわち、次の疑似コードとして: [...]

おそらくいいえ。VC++ 2012 ラムダ型は、1 つのラムダ本体実装 ( void operator()()) のみを提供しますが、変換の呼び出しごとに複数の「関数ポインターへのユーザー定義の変換」を提供します (演算子は、、、および型の関数ポインターを返しvoid (__fastcall*)(void)ますvoid (__stdcall*)(void)) void (__cdecl*)(void)

以下に例を示します。

// input source code
auto lm = [](){ /*lambda-body*/ };

// reversed C++ code from VC++2012 output assembly code
class lambda_UNIQUE_HASH {
  void __thiscall operator()() {
    /* lambda-body */
  }
  // user-defined conversions
  typedef void (__fastcall * fp_fastcall_t)();
  typedef void (__stdcall * fp_stdcall_t)();
  typedef void (__cdecl * fp_cdecl_t)();
  operator fp_fastcall_t() { ... }
  operator fp_stdcall_t() { ... }
  operator fp_cdecl_t() { ... }
};
lambda_UNIQUE_HASH lm;
于 2013-02-13T03:41:54.027 に答える
3

ステートレス ラムダ関数もクラスですが、関数ポインターに暗黙的に変換できるクラスです。

C++ 標準は呼び出し規則をカバーしていませんが、ラムダが関数ポインターに変換されたときにステートレス ラムダに転送される呼び出し規則で、ステートレス ラムダがラッパーを作成できない理由はほとんどありません。

例として、これを行うことができます:

#include <iostream>

void __cdecl h1() {}
void __stdcall h2(){}

// I'm lazy: 
typedef decltype(&h1) cdecl_nullary_ptr;
typedef decltype(&h2) stdcall_nullary_ptr;

template<typename StatelessNullaryFunctor>
struct make_cdecl {
  static void __cdecl do_it() {
    StatelessNullaryFunctor()();
  }
};
template<typename StatelessNullaryFunctor>
struct make_stdcall {
  static void __stdcall do_it() {
    StatelessNullaryFunctor()();
  }
};

struct test {
  void operator()() const { hidden_implementation(); }

  operator cdecl_nullary_ptr() const {
    return &make_cdecl<test>::do_it;
  }
  operator stdcall_nullary_ptr() const {
    return &make_stdcall<test>::do_it;
  }
};

ここで、ステートレスな nullary クラスは暗黙的に aと関数ポインターtestの両方に変換できます。cdeclstdcall

これの重要な部分は、呼び出し規約が関数ポインターの型の一部であるため、operator function_typeどの呼び出し規約が要求されているかを知っていることです。そして、完全な転送により、上記は効率的ですらあります。

于 2013-02-13T03:43:36.580 に答える