哪些类型的C++函数可以放置在C函数指针中?

7

我有一个使用函数指针结构体来进行回调的C库。这些回调函数将会被C代码调用。

extern "C" {
typedef struct callbacks_t {
    void (*foo) (const char*);
    int  (*bar) (int);  
} callbacks_t;
}// extern C

我可以放心地把什么样的C++函数放在这些函数指针中,以便从C库中调用?静态成员函数?完全指定的模板函数?非捕获Lambda表达式?

看起来g++让我可以使用上述所有内容,但当C和C ++函数使用不同的调用约定和语言绑定时,我对其安全性产生了疑问。


1
但是我对在调用C和C++函数时使用不同的调用约定和语言绑定的安全性提出了质疑。当所有内容都针对目标环境进行一致编译时,就不会出现这样的差异。 - πάντα ῥεῖ
3
静态成员函数、特化的模板函数和非捕获lambda不能使用extern "C"调用约定,因此在形式上不太好。但实际上它取决于编译器。在实践中,主要问题并不是它不能工作,而是一些编译器可能会对形式产生愚蠢的警告。 - Cheers and hth. - Alf
2
@Cheersandhth.-Alf,这应该不重要。C库应该能够调用由C++编译器混淆名称的函数而没有任何问题。 - R Sahu
4
不是关于名称混淆,而是关于调用约定。 - Cheers and hth. - Alf
@Cheersandhth.-Alf,我很好奇。是否有平台在C与C++中使用不同的调用约定? - R Sahu
4
我从未听说过这样的事情。据报道(在 clc++ 讨论时),有一个编译器会发出愚蠢的警告,我相信它是为 Solaris 准备的。因此,我认为这与有符号整数的一补码或二进制补码表示形式的可能性问题处于同一类问题中。 - Cheers and hth. - Alf
2个回答

7
我有一个使用函数指针结构体作为回调的C库。这些回调将从C代码中被调用。
C库只能理解C语言,因此您只能传递C明确支持和理解的内容。
由于在C++中没有任何函数类型的调用约定定义,因此您不能显式地传递C++函数。您只能传递C函数(这些函数明确声明为extern "C"),并保证它们是兼容的。
与所有未定义行为一样,这可能看起来可以工作(例如传递普通的C++函数或静态成员)。但就像所有未定义的行为一样,它被允许工作。您只是不能保证它实际上是正确的或可移植的。
extern "C" {
    typedef struct callbacks_t {
        void (*foo) (const char*);
        int  (*bar) (int);  
    } callbacks_t;

    // Any functions you define in here.
    // You can set as value to foo and bar.

}// extern C

哪些类型的C++函数可以安全地放置在这些函数指针中,以便从C库中调用?
静态成员函数吗?
不行。但是在许多平台上,它确实有效。这是常见的bug,并且会在某些平台上咬人。
完全指定的模板函数呢?
不行。
不捕获的Lambda呢?
不行。
g ++似乎让我使用以上所有内容,
是的。但是它假设您正在传递使用C ++编译器构建的对象(这是合法的)。由于C ++代码将使用正确的调用约定,因此问题在于当您将这些内容传递给C库时(例如pthreads)。

几年前,您提到过遇到一个使用不同于C语言调用约定的静态成员函数的C++编译器:http://stackoverflow.com/questions/1738313/c-using-class-method-as-a-function-pointer-type/1738425#comment-1618920。虽然这已经是很久以前的事情了,但我认为如果您能回忆起任何相关细节,那将非常有趣。 - Michael Burr
1
这是我在VERITAS工作时的经历(4个工作前)。我负责构建系统,该系统可以在25种编译器/操作系统变体上构建软件。不幸的是,我无法记住具体是哪一个。但是,这种经验基本上让我远离任何接近未指定或未定义行为的东西。 - Martin York

3
通常情况下,除非你使用cast,否则你应该信任g++。
虽然你提到的函数类型都不能从C中导出使用,但这不是你要问的。你想知道可以作为函数指针传递的函数。
为了回答你能够传递什么,我认为更有建设性的方法是理解你不能传递什么。你不能传递需要在参数列表中未明确声明的其他参数的任何内容。
所以,没有非静态方法。它们需要一个隐式的“this”。C不会知道如何传递它。然而,编译器也不会让你这样做。
没有捕获lambda。它们需要一个隐含的参数和实际的lambda主体。
你可以传递不需要隐式上下文的函数指针。事实上,你已经列出了它们:
- 函数指针。无论是标准函数还是模板,只要模板被完全解析即可。这不是问题。你写的任何语法都将自动完全解析模板,从而得到函数指针。 - 非捕获lambda。这是C++11引入lambda时引入的一种特殊的解决方法。由于可能这样做,编译器执行所需的显式转换,使其发生。 - 静态方法。由于它们是静态的,所以它们不会传递隐式的"this",所以它们是可以的。
最后一个需要扩展。许多C回调机制得到一个函数指针和一个void* opaq。以下是一种标准且相当安全的使用C++类的方法:
class Something {
  void callback() {
    // Body goes here
  }

  static void exported_callback(void *opaq) {
    static_cast<Something*>(opaq)->callback();
  }
}

接下来执行:

Something something;

register_callback(Something::exported_callback, &something);
编辑: 这个方法有效的原因是C++调用约定和C调用约定在不传递隐式参数时是相同的。虽然名称混淆有所不同,但当您传递函数指针时,它并不相关,因为名称混淆的唯一目的是允许链接器找到正确的函数地址。
如果您尝试使用期望stdcall或pascal调用约定的回调尝试这种技巧,则此方案将失败。
然而,这不仅适用于静态方法,lambda和模板函数。即使是标准函数在这种情况下也会失败。
遗憾的是,当您定义一个指向stdcall类型的函数指针时,gcc会忽略您。
#define stdcall __attribute__((stdcall))
typedef stdcall void (*callback_type)(void *);

导致的结果是:

test.cpp:2:45: warning: ‘stdcall’ attribute ignored [-Wattributes]
 typedef stdcall void (*callback_type)(void *);

1
这是非常实践的观点。并不是说错了。但在正式场合中,不能将调用约定与明确定义的行为混合使用。我认为你还应该提到正式的情况。这也值得了解。 - Cheers and hth. - Alf
1
这完全是错误的。你不能通过函数指针将任何C++函数传递给C代码。这是因为两种语言的调用约定在标准中没有规定。你只能从C++传递C函数并保证它可以工作(显式声明为extern "C")。它会出问题吗?也许,也许不会,就像所有未定义行为一样,在你的平台上似乎可以完美地工作。 - Martin York
@LokiAstari 这是我预期的答案,也是我害怕的。我继续为我的所有回调编写了一些小的 extern "C" 包装器,以转发到我的 C++ 函数。C++ 函数在包装器内被内联,因此没有开销,只是有些烦恼必须这样做。 - user1526370

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