C++ lambda 函数的默认调用约定是啥?

Posted

技术标签:

【中文标题】C++ lambda 函数的默认调用约定是啥?【英文标题】:What's the default calling convention of a C++ lambda function?C++ lambda 函数的默认调用约定是什么? 【发布时间】:2013-01-28 12:51:48 【问题描述】:

以下代码使用 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 是异常的。

所以,我的问题是:

    C++ lambda 函数的调用约定是什么?

    如何指定 C++ lambda 函数的调用约定?

    如果没有定义调用约定,如何在调用lambda函数后正确回收堆栈空间?

    编译器会自动生成多个版本的 lambda 函数吗?即如下伪代码:

    [] __stdcall ();

    [] __cdecl ();等等

【问题讨论】:

链接这个问题似乎很有用:***.com/questions/14169295/… f1 使用的是__stdcall,但h1 使用的是__cdecl;如果你交换它们,它会起作用吗? 由于 C++ 标准没有提及任何关于调用约定的内容,因此您的第一个问题的答案似乎是编译器特定的。 我认为@jogojapan 链接的问题回答了这个问题。查看发布在其中的符号转储,当您定义 lambda 时,似乎 VC 会生成具有所有 3 种调用约定的函数调用运算符。它将根据调用 lambda 的方式链接最合适的匹配项。 @jogojapan,很抱歉我忽略了您提供的链接。我已为您的评论点赞并删除了我的第一条评论。 【参考方案1】:

在 VC++ 2012 上,编译器选择自动调用无状态 lambda(没有捕获变量)的转换您将“无状态 lambda 转换为函数指针”。

MSDN C++11 Features:

Lambdas

[...] 此外,在 Visual Studio 2012 的 Visual C++ 中,无状态 lambda 可转换为函数指针。 [...](Visual Studio 2012 中的 Visual C++ 甚至比这更好,因为我们已将无状态 lambda 转换为具有任意调用约定的函数指针。当您使用期望像 @987654324 这样的 API 时,这一点很重要@函数指针。)


已编辑:

注意:调用转换超出C++标准,依赖于平台ABI(应用程序二进制接口)等其他规范。

以下答案基于带有/FAs compiler option 的输出汇编代码。 所以这只是一个猜测,请向微软询问更多细节;P

第一季度。 C++ lambda 函数的调用约定是什么?

第三季度。如果没有定义调用约定,在调用 lambda 函数后如何正确回收堆栈空间?

首先,C++ lambda(-expression) 不是函数(也不是函数指针),你可以像调用普通函数一样调用operator() 到lambda 对象。 并且输出汇编代码显示VC++ 2012 生成带有__thiscall 调用转换的lambda-body。

第二季度。如何指定 C++ lambda 函数的调用约定?

AFAIK,没有办法。 (可能只有__thiscall

第四季度。编译器会自动生成多个版本的 lambda 函数吗?即作为以下伪代码:[...]

可能没有。 VC++ 2012 lambda-type 只提供了一种 lambda-body 实现(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;

【讨论】:

+1 供参考(尽管答案并未涵盖问题的所有方面)。 @jogojapan:它几乎涵盖了所有内容,因为 lambda 函数最终仍然是一个成员函数。并且成员函数并没有真正的调用约定。不是__cdecl 方式。由于调用约定无论如何都是特定于平台的,因此由每个平台决定它的工作方式。微软显示出令人震惊的程度的能力,决定采用最有用的方法。 @NicolBolas 我的意思主要是引用的文字似乎没有为问题的第 2 部分提供明确的答案。上面的描述说,默认情况下 lambda 可以转换为所有调用约定,但这并不一定意味着如果想要指定约定,就无法指定。当然,这将是特定于 VC 的语法。 (我不确定我是否理解您关于成员函数的观点。Lambda 始终是成员函数。为什么会这样?) @jogojapan: "Lambdas 总是成员函数.. 为什么会这样?" 因为 C++ 中的 lambda 函数是 functors;他们是对象。他们有一个operator(),这是您的功能所在。即使是无捕获的 lambda 也是对象。它们可转换为函数指针,但它们不是函数指针。它们是物体。除了可转换性之外,lambda 在功能上与struct Nameless Ret operator()(...) ...; 没有什么不同。事实上,该标准要求它以这种方式实现,带有类型名和所有内容。 另见this reference【参考方案2】:

无状态的 lambda 函数仍然是一个类,但是一个可以隐式转换为函数指针的类。

C++ 标准不涵盖调用约定,但几乎没有理由说明为什么无状态 lambda 无法在任何调用约定中创建包装器,当 lambda 转换为函数指针时,该包装器转发到无状态 lambda。

例如,我们可以这样做:

#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;
  
;

我们的test 无状态空值类可以隐式转换为cdeclstdcall 函数指针。

其中重要的部分是调用约定是函数指针类型的一部分,因此operator function_type 知道正在请求什么调用约定。而通过完美转发,上述方法甚至可以是高效的。

【讨论】:

一个 lambda 函数是一个类吗? @jogojapan:应该说,一个无状态的lambda仍然是一个object,但是一个可以转换成函数指针的对象。

以上是关于C++ lambda 函数的默认调用约定是啥?的主要内容,如果未能解决你的问题,请参考以下文章

C#通过PInvoke调用c++函数的备忘录

(Visual) C++ 中动态创建函数的调用约定

调用约定

如何在 Visual C++ 中使用 Delphi 的寄存器调用约定调用函数?

使用 Node.JS 调用 AWS 胶水的 lambda 函数不使用 console.log 的原因是啥?

__stdcall的名字修饰约定