在C ++中,可变参数函数(在参数列表末尾带有...的函数)必须遵循__cdecl调用约定吗?

时间:2010-03-25 02:22:20

标签: c++ variadic-functions stdcall cdecl

我知道__stdcall函数不能有省略号,但我想确保没有支持stdarg.h函数的平台来调用除__cdecl或__stdcall之外的约定。

5 个答案:

答案 0 :(得分:8)

调用约定必须是调用者从堆栈中清除参数的约定(因为被调用者不知道将传递什么)。

但这并不一定与微软称之为“__cdecl”的内容相对应。例如,在SPARC上,它通常会传递寄存器中的参数,因为这就是SPARC设计工作的方式 - 它的寄存器基本上充当调用堆栈,如果调用足够深,它会溢出到主存储器他们将不再适合登记。

虽然我不太确定它,但我期望在IA64(Itanium)上大致相同 - 它也有一个巨大的寄存器集(如果内存服务则为几百)。如果我没有弄错的话,它对你如何使用寄存器更加宽容,但我希望它至少在很多时候都能被类似地使用。

为什么这对你很重要?使用stdarg.h及其宏的目的是隐藏调用约定与代码之间的差异,因此它可以移植地使用变量参数。

编辑,根据评论:好的,现在我明白你在做什么(至少足以改善答案)。鉴于您已经(显然)拥有处理默认ABI变体的代码,事情就更简单了。这只留下了可变函数是否总是使用“默认ABI”的问题,无论发生在手头平台上的是什么。以“stdcall”和“default”为唯一选项,我认为答案是肯定的。例如,在Windows上,wsprintfwprintf打破了经验法则,并使用cdecl调用约定而不是stdcall。

答案 1 :(得分:2)

您可以确定这一点的最明确方法是分析调用约定。要使可变函数起作用,您的调用约定需要几个属性:

  • 被调用者必须能够从堆栈顶部的固定偏移量访问不属于变量参数列表的参数。这要求编译器将参数从右向左推入堆栈。 (这包括printf的第一个参数,格式规范。此外,变量参数列表本身的地址也必须从已知位置派生。)
  • 一旦函数返回,调用者必须负责从堆栈中删除参数,因为只有编译器在为调用者生成代码时才知道首先将多少参数压入堆栈。可变函数本身没有这些信息。

stdcall将无效,因为被调用者负责从堆栈中弹出参数。在旧的16位Windows时代,pascal无效,因为它将参数从左向右推入堆栈。

当然,正如其他答案所暗示的那样,许多平台在调用约定方面没有给你任何选择,使得这个问题与那些问题无关。

答案 2 :(得分:1)

在x86系统上考虑以下功能:

void __stdcall something(char *,...);

该函数将自身声明为__stdcall,这是一个callee-clean约定。但是变量函数不能被callee-clean,因为被调用者不知道传递了多少参数,所以它不知道它应该清理多少。

Microsoft Visual Studio C / C ++编译器通过将调用约定静默转换为__cdecl来解决此冲突,这是唯一支持的函数调用约定,这些函数不会隐藏此参数。

为什么这种转换是以静默方式进行而不是生成警告或错误?

我的猜测是将编译器选项/ Gr(将默认调用约定设置为__fastcall)和/ Gz(将默认调用约定设置为__stdcall)减少烦恼。

将可变参数函数自动转换为__cdecl意味着您只需将/ Gr或/ Gz命令行开关添加到编译器选项中,所有内容仍然可以编译和运行(只需使用新的调用约定)。

另一种看待这种情况的方法不是将编译器视为将variadic __stdcall转换为__cdecl,而是简单地说“对于可变参数函数,__ stdcall是调用者干净的。”

click here

答案 3 :(得分:0)

您的意思是'MSVC支持的平台'还是一般规则?即使您只限于MSVC支持的平台,您仍然会遇到IA64AMD64等情况“一个”调用约定,并且调用约定被称为__stdcall,但它肯定与x86上的__stdcall不一样。

答案 4 :(得分:0)

AFAIK,调用约定的多样性对于x86上的DOS / Windows是唯一的。大多数其他平台都有编译器附带操作系统并标准化惯例。