在C++中,对于C API回调,使用静态成员函数指针是安全的/可移植的吗?静态成员函数的abi是否与c函数相同?
- 有人告诉我,有些英特尔编译器无法使用静态成员函数:mail.gnome.org/archives/gtk-list/2011-march/msg0085.html
它是不安全的每一个C++标准。如本公告所述:
A C callback function implemented in C++ must be extern"C". It may seem to work as a static function in a class because class-static functions often use the same calling convention as a C function. However, doing that is a bug waiting to happen (see comments below), so please don't - go through an extern"C" wrapper instead.
根据马丁·约克在回答中的评论,在某些平台上尝试这样做存在现实问题。
使您的cABI回调cx1〔0〕。
编辑:从标准中添加一些支持报价(强调我的报价):
3.5"程序和链接":
After all adjustments of types (during which typedefs (7.1.3) are replaced by their definitions), the types specified by all declarations referring to a given object or function shall be identical, except that declarations for an array object can specify array types that differ by the presence or absence of a major array bound (8.3.4). A violation of this rule on type identity does not require a diagnostic. [3.5/10]
[Note: linkage to non-C++ declarations can be achieved using a linkage-specification (7.5). ] [3.5/11]
和
7.5"联动装置规范":
... Two function types with different language linkages are distinct types even if they are otherwise identical. [7.5/1]
因此,如果使回调的代码使用回调的C语言绑定,那么回调目标(在C++程序中)也必须如此。
- 感谢这个链接——不过,在我看来,在实践中,所有的编译器(我所有的工作对象……)似乎都提供了不可移植的解决方案——比如调用约定声明——来解决问题。
- @彼得森:问题可以用extern"C"来解决,还是有什么我遗漏的?
- "按C++标准"?标准的哪一部分说明了这一点?
- @罗杰:在讨论"语言链接"时,7.5/3说"每个实现都应该提供到用C编程语言编写的函数的链接",这意味着必须支持extern"C"。
- 你所引用的并不是说使用静态方法是不安全的。
- @罗杰:我想我误解了你的要求了——我在标准中加了一些我认为可以解决你问题的引言。
- 当最好的支持是一个注释(这是非规范性的,不具有约束力)时,我总是感到沮丧。但这有帮助,谢谢。我很高兴我没有在一个让人非常尴尬的地方工作(比如在你链接的帖子上的评论),因为语言使一个问题变得如此模糊而感到困惑。
- @罗杰:我找到了标准的一个规范性部分,用它代替了非规范性注释。
- 谢谢,迈克尔。在我接受你的答案之前,我需要仔细分析你的答案和引语,但到目前为止它看起来不错。
- 米迦勒:你想说C++标准中的3.5/9覆盖了用一种完全不同的语言编写的回调代码吗?另外,这只讨论声明,我不确定它如何应用于向函数传递参数。
- 3.5/10,事实上,我已经在你的答案中修正了参考文献。
- C++标准只覆盖C++程序,但显然声明必须与调用方所使用的调用约定相匹配(即使调用方不是用C或C++编写的)。如果调用方使用C调用约定(标准调用的是"语言链接"),那么它必须在C++程序中声明EDCOX1 0。平台将定义什么是C链接约定。如果调用者使用了其他东西(这不是我理解这个问题要问的问题),那么extern"C"就不合适了,您将进入一个不可移植的实现定义区域。
- 注意:我认识到静态成员通常会工作(我总是在Windows上思考)。在我链接到的帖子中,我首先在我的答案中提出了同样多的建议,但马丁表示他会遇到实际的不起作用的实现,所以我修改了我的答案,建议避免将它们用作回调(它从语言律师问题变为现实世界)。我不会轻视任何使用这种技术的人——我过去确实做过很多次。但是,我希望如果它出现在代码评审中,那么就可以使用extern"C"包装器。即使在窗户上。
- 迈克尔:明白,这不是问题所在;我不是要挑你的答案,但你在努力改进它,我很高兴知道它确实是标准所要求的以及标准在哪里要求它。否则,我认为在我回答这个问题时,您必须退回到"完全超出标准的范围并达到实现要求"。
- 我能找到的最好的结果是,您不应该能够转换函数指针类型(它应该是编译时错误),但是即使是模糊的——基于7.5/1,并且没有看到任何允许转换的内容。此外,3.5似乎越来越不相关。
- @罗杰:对我来说,3.5和7.5的读数似乎很清楚(事实上,我认为这是7.5存在的一个主要原因)。但我会第一个说我在语言和标准上没有权威,可能我读的这些段落比在那里读的要多(尽管现在我不认为自己是)。但是,对于C回调来说,使用extern"C"包装器似乎毫无疑问是安全的,而使用静态成员则存在一些不确定性。也许问题应该转一转——标准是否明确允许C回调的静态成员?
- 它们都使用"链接",但3.5是关于内部/外部/无链接(这对ODR的定义很重要),7.5是关于使"语言链接"在3.5框架内工作(特别是阅读7.5/2的第一句话)。7.5/1的"一些属性"在没有指定实现的情况下无疑是安全的。特定于每个实现,此处不描述。例如,。一种特殊的呼叫约定等。"不幸的是,这只是一个注释,而且标准的规范性部分似乎只通过省略来说明这一点。
- 同样来自7.5/1:"所有函数类型、函数名和变量名都有语言链接。"这是除了外部/内部/"无链接"之外,还强调了标准使用"链接"的两种方式的正交性,至少对我来说是这样。
在对其他问题进行搜索和几次休息之后,我找到了一个清晰简洁的答案(无论如何,对于标准语言来说):
Calling a function through an expression whose function type has a language linkage that is different from the language linkage of the function type of the called function's definition is undefined. [5.2.2/1]
我仍然认为,在一个基本层面上,使用C++中的文本来定义C编译器编译的C库的行为是有问题的,确切地说,中间语言互操作如何工作是非常具体的;然而,这是我认为最接近的标准(当前)希望定义这样的中介。方法。
特别是,这是未定义的行为(并且不使用C库,因此不会出现问题):
1 2 3 4
| void call(void (*pf)()) { pf(); } // pf() is the UB
extern"C" void f();
int main() { call(f); }
// though I'm unsure if a diagnostic is required for call(f) |
comeau确实在call(f)上给出了诊断(尽管它可以做到,即使不需要诊断)。
这不是未定义的行为,并显示了如何在函数指针类型(通过typedef)中包含语言链接:
1 2 3 4
| extern"C" typedef void F();
void call(F* pf) { pf(); }
extern"C" void f();
int main() { call(f); } |
或者可以写:
1 2 3 4 5 6
| extern"C" {
typedef void F();
void f();
}
void call(F* pf) { pf(); }
int main() { call(f); } |
对于所有我知道的Windows C++编译器来说,答案是肯定的,但是语言标准中没有任何东西可以保证这一点。但是,我不想让它阻止你,这是使用C++实现回调的一种非常常见的方式,但是你可能发现你需要声明静态函数为WiAPI。这是从我自己的旧线程库中获取的:
1 2 3 4
| class Thread {
...
static DWORD WINAPI ThreadFunction( void * args );
}; |
这里是TE Windows线程API使用的回调。
- 我失去了链接,但是gcc(在最新版本中)也使用了相同的静态方法和C函数调用约定,所以gcc用户也是安全的。
- 但我不太明白这一点。我的意思是,如果函数是一个静态成员,为什么不安全地运行它,并且首先通过一个非成员函数来实现它呢?
- JALF:因为你仍然只有安全的假象,因为你是在实现的突发奇想中。显然,它的便携性稍高一点(我还没有听说这会影响到哪些编译器),但是,正如我确信的那样,这与标准所保证的不同。为什么要花很大的精力来解决那些甚至没有出现在您的实现中的问题,而这些问题在未来5年内您不会受到影响?
- 也就是说,我很感兴趣,因为我想了解语言的黑暗角落,而不仅仅是我使用的实现,我认为这是其他人深入分析这一点的相同或类似原因。:)
- @罗杰:"很痛!"如果您调用在头文件中移动两行静态方法声明,并用外部的"c"对其进行前缀,那么编码一定是一种偏头痛。
- @罗杰:我也不同意"安全的假象"。外部"c"规定了7.5.3"每个实现应提供与用c编程语言编写的函数的链接"。
- 我是这么看的。没有额外的费用(正确的样式更改)。结果是有保证的。回调函数是对C类库的一种回退,它不能保证类型安全,因此在我们需要与C库接口时才出现在现代C++代码中是不常见的。由于我们与C库的接口是对称的,因此应该使用C函数。
- 马丁:在真正的代码中从来没有这么简单。我使用一个很好的模板技巧来为我生成这些,但是它需要使用静态成员函数,并且不能用于非成员。在这里,它被剥离到最基本的:codepad.org/v7kykd0v。
- 马丁:这只是安全的假象,因为除非你确切地知道C库是如何编译的,否则你不知道你的编译器使用的是正确的ABI。但是,如果您知道您的编译器和那个编译器,那么您就可以知道它是否工作。当你试图写100%的可移植代码时,你不知道哪一个编译器。
- 马丁:也许你在产生这个问题的问题上错过了它:这个问题是在什么编译器中中断的?(我真的想知道,在我必须找出困难的方法之前。:)
- (我说错了,它可以作为一个非成员实现,但C链接不能作为14/4特别禁止这样做。)
- @罗杰:我不同意你必须知道C库是如何编译来了解ABI的。ABI不是由编译器定义的,而是由它使用的标准库定义的。编译器必须符合这个标准(因为标准库是二进制的而不是源代码),因此,如果您知道正在使用的标准库,那么您就知道C-ABI是什么,因此C和C++编译器都将使用相同的C-ABI。
- 它会破坏什么编译器。在上一份工作中,我在25个编译器/OS/硬件配置上编译了ace/tao和一些公司代码。在每个配置中内置8种(调试/发布-单线程多线程-共享/静态lib)风格。在这200个版本中,我在3个版本中发现了这个问题(我在3个版本中发现了一个可复制的问题,我可以调试),它可能会影响到其他版本,但我花了很长时间才发现问题并解决。确切的编译器/版本让我难以理解(那是5年前的事了),但我认为它是一个较旧的Sun编译器和一个较旧的AIX编译器,我可能错了。它们是编译器的旧版本
- 将它从编译器转移到stdlib并没有改变太多(并且通过说"platform"部分地覆盖了它)——您仍然可以有几个不同的stdlib实现,并且您仍然依赖于标准或代码中没有指定的东西。或者换句话说,必须为该平台编译外部库(原始示例中的GLUT),并且必须确保它正确编译,以便您可以调用它。谢谢你提供的信息和提出这个,我了解了很多关于这个黑暗角落。
- @马丁:那真是个战争故事!
- 罗杰:所以你是说,假设STD库是正确的,C和C++编译器都符合标准,并且使用同一个ABI作为STD库,它们没有bug,那么所有的事情都应该工作。我认为这是你必须做的一个假设。这些是我们在上面构建的基本元组件。如果上面的任何一个失败了,那么什么都不会起作用。
ABI并没有被C或C++标准所覆盖,即使C++确实通过EDCOX1 0给出了"语言链接"。因此,ABI基本上是特定于编译器/平台的。这两个标准都有许多,许多事情需要实现,这就是其中之一。
因此,编写100%可移植代码或切换编译器是不可能的,但允许供应商和用户在其特定产品中具有相当大的灵活性。这种灵活性允许更多的节省空间和时间的项目,这些方式不需要标准委员会预先预测。
据我所知,ISO的规则不允许标准每10年一次以上(但可以有各种出版物,如C++的TC1和Tr1)。此外,还有一个想法(我不确定这是否来自于ISO,是由C委员会,甚至是其他地方推行的)是"提炼/标准化现有实践,而不是进入左领域,并且有许多现有的实践,其中一些存在冲突。
- 嗯,我不同意马丁的观点,多年的Windows编程经验似乎可以证明这一点。正如您所观察到的,没有标准的ABI,所以使用C函数也没有任何保证。
- 如果你只使用Windows编译器,你会没事的…在将代码移植到Android、Linux或Mac或其他设备上之前,您可能会发现"正在工作"的代码不起作用。使用extern c最安全-这并不是什么额外的工作。
- 尼尔:没错。你能做的最好的就是说"确保你按照编译器的要求去做"。
- extern C很好,但Martin似乎想坚持让它们成为非成员函数——或者我读错了?
- 您可以声明一个静态成员函数extern"c"?不知道。
- 埃米尔:你不能,只能在命名空间范围内,这似乎就是为什么他坚持不使用静态成员的原因。
- 好吧,我原以为你可以,但看来我好像有点昏昏欲睡。
- 事实上,显然你不能-名字必须被破坏。
- @gbjbaanb:如果您想移植使用回调在Linux、Android、MacOS等环境下工作的Windows代码,回调是静态成员函数,而不是全局外部"c"函数,这将是您遇到的问题中最小的一个(当然,除非您从Qt或WxWidgets开始,在这种情况下,您可能不会编写这样的回调回调)。
- @尼尔-假设回调API使用C ABI,那么extern"C"是完全可移植的,并且由标准指定。当然,如果回调不使用c abi,那么您肯定是在不可移植的地方,所以可以做任何工作。
- @迈克尔,我真的不想参加ABI讨论(我的大脑显然快关机了),所以-晚安!
- 迈克尔:没有c-abi,在特定的平台上有c-abi。我认为你混淆了C调用约定(这不是extern"C"所规定的),而Martin所说的不是C调用约定。
- 是-ABI特定于平台。在一个特定平台上编译一个带有EDCOX1×0的函数声明的C++程序将期望该函数使用该平台的C abi。
- 尼尔:不,这不明显,甚至还有一个医生在说:open-std.org/jtc1/sc22/wg21/docs/cwg-closed.html 168。
- @尼尔:给自己留个言……要让尼尔B退出讨论/争论,请带上ABIS。如果需要额外推一点,请讨论typedef结构。P
- @NIEL:如果我们可以在静态方法上使用extern"c",我就不会对它们有任何问题。但我似乎做不到。也许我没有正确的语法。
- @罗杰:C ABI可能没有在标准中明确定义(因为它通常与支持它的底层硬件相链接),但它必须由实现来定义,这受到它使用的标准库的限制(即鸡和蛋[编译器定义ABI并构建标准库还是标准库定义一个因此编译器必须遵循约定?)无论如何,C++标准保证了外部"C"提供了与C中的函数的链接。这不仅仅是调用约定,而且是ABI。
- 如前所述,不能在成员函数上指定语言链接。(回复:在此之前的第二条评论。)
- 马丁:你提到的确切问题是编译器,它们有自己的高度优化的abi并在寄存器中传递参数。一旦您接受外部库可以编译为使用与当前TU不同的ABI,那么您所做的任何事情都取决于实现细节,因为标准没有明确定义ABI。问题不是提供与C中编写的函数的链接,而是提供从C函数到C++编写的函数(静态成员函数)的链接。
- 罗杰:这是我的价值所在——因为一个链接规范只能在命名空间范围内发生,成员函数不能指定一个语言链接,因此它们得到默认的C++链接。如果静态成员函数在被C函数调用时工作正常,那么这只是运气(或者,如果供应商承诺它会工作的话,可能是语言扩展)。类似于如果a = a++ + ++a做了你所期望的(不管是什么,它给了我五次中的四次5)。
- 是的,我已经了解到,就标准而言,它完全是一个实现细节,这是本次讨论的结果(我很高兴了解到这一点),因此我完全同意您的观点。我现在看到的是标准对extern"C" typedef void (*Callback)(); extern"C" void reg(Callback); void f() {} int main() { reg(f); }提出的要求,据我所知,它以一种不允许的方式转换函数类型,并且由于没有隐式转换序列(13.3.2/3),因此会导致13.3/4中的程序格式不正确。
- 事实上,COMEAU在严格模式下诊断出这一点,但允许它在非严格模式下。(comeacomputing.com/tryitout)