C++ lambda函数的默认调用惯例是什么?

17

以下代码是使用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
}

我认为错误是正常的,而成功则是异常的。

因此,我的问题是:

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

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

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

  4. 编译器是否会自动生成多个版本的lambda函数?例如以下伪代码:

    [] __stdcall (){};

    [] __cdecl (){}; 等等。


3
链接此问题似乎很有用:https://dev59.com/n2Yq5IYBdhLWcg3w0T1y - jogojapan
@jogojapan,我的意思是这个问题的重点与那个不同。至于第二个问题,是的,你说得对。 - xmllmx
5
由于C++标准没有提及任何有关调用约定的内容,因此对于你第一个问题的答案似乎会变得与编译器相关。 - Jesse Good
3
我认为@jogojapan链接的问题已经回答了这个问题。看一下那个问题中发布的符号转储,似乎VC在定义lambda时会生成带有所有3种调用约定的函数调用运算符。根据lambda的调用方式,它将链接最合适的匹配。 - Praetorian
1
@jogojapan,非常抱歉我忽略了您提供的链接。我已经点赞了您的评论并删除了我的第一条评论。 - xmllmx
显示剩余6条评论
2个回答

16

在VC++ 2012中,编译器会自动选择调用转换来处理无状态捕获变量的lambda表达式,你将“无状态lambda表达式转换为函数指针”时。

MSDN C++11 Features:

Lambdas

[...] 此外,在Visual Studio 2012中的Visual C++中,无状态lambda表达式可以转换为函数指针。[...] (Visual Studio 2012中的Visual C++甚至更好,因为我们使无状态lambda表达式可以转换为具有任意调用约定的函数指针。这在使用期望像__stdcall函数指针的API时非常重要。)


编辑:

NB:调用转换不符合C++标准,它取决于其他规范,例如平台ABI(应用程序二进制接口)。

以下答案基于使用/FAs编译器选项的输出汇编代码。因此这只是一个猜测,请向Microsoft询问更多细节;P

Q1. C++ Lambda函数的调用约定是什么?

Q3. 如果未定义调用约定,则如何在调用Lambda函数后正确回收堆栈空间?

首先,C++ lambda(表达式)不是函数(也不是函数指针),您可以像调用普通函数一样调用operator()到lambda对象。 并且输出汇编代码显示VC++ 2012使用__thiscall调用转换生成lambda体。

Q2. 如何指定C++ Lambda函数的调用约定?

据我所知,没有办法。(可能只有__thiscall

问题4:编译器是否会自动生成多个lambda函数版本?即如下伪代码:

[...]

可能不会。VC++ 2012的lambda类型只提供一个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;

2
+1 为该参考答案点赞(尽管该答案并未涵盖问题的所有方面)。 - jogojapan
5
它基本上涵盖了所有内容,因为 lambda 函数最终仍然是成员函数。而成员函数并没有真正的调用约定。不像 __cdecl 那样。由于调用约定是特定于平台的,因此每个平台决定如何工作。微软表现出了惊人的能力,决定采用最有用的方式。 - Nicol Bolas
@NicolBolas 我的意思主要是引用的文本似乎没有清晰地回答问题的第二部分。上面的描述说,lambda默认情况下可转换为所有调用约定,但这并不一定意味着如果有人想要指定约定就没有办法了。当然,这将是VC特定的语法。(我不确定我是否理解了您关于成员函数的观点。Lambda始终是成员函数..这是为什么呢?) - jogojapan
1
@jogojapan:“为什么Lambda函数总是成员函数?”因为C++中的Lambda函数是函数对象,它们是对象。它们有一个operator(),这就是你的函数所在的地方。即使是没有捕获的Lambda也是对象。它们可以转换为函数指针,但它们不是函数指针,它们是对象。除了可转换性外,Lambda在功能上与struct Nameless {Ret operator()(...) {...}};没有任何区别。实际上,标准要求以这种方式实现Lambda,包括typename和其他所有内容。 - Nicol Bolas
1
还可以参考这个参考链接 - Shafik Yaghmour
显示剩余2条评论

3

无状态的 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知道正在请求哪种调用约定。而且,使用完美转发,以上甚至可以高效实现。


1
一个lambda函数是一个类吗? - jogojapan
5
无状态 Lambda 仍然是一个 对象,但它可以转换为函数指针。 - Jesse Good

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接