使用模板摆脱动态链接样板​​文件:extern "C" 和模板不要混用;

Using templates to get rid of dynamic linking boilerplate: extern "C" and templates don't mix;

我想到了一些 C++ 代码,起初我并没有考虑太多,直到我注意到 clang 接受它但 gcc 不接受。我越想越觉得 gcc 可能是对的。问题是,如果没有大量样板文件,我真的想不出一个好的、合法的方法来做我想做的事。

首先,这是我希望用 g++ 编译的代码:

extern "C" template <typename T>
using factory_pointer_t = T* (*)();

因为我认为这可能是一个 XY 问题,所以有点背景知识。我正在开发一个界面相当简单的小插件系统。基本上它只包含一个工厂函数,return 是一个指向抽象基础 class 的指针。抽象基 class 要求处理内存管理的方法,类似于 COM。像这样:

class ITest {
public:
    virtual void release() = 0;
    virtual void test_method() = 0;
protected:
    virtual ~ITest(){}
};

这将在动态库中实现,如下所示:

class Test : public ITest {
public:
    virtual void release() override {
        delete this;
    };
    virtual void test_method() override {
        std::cout << "Hello, shared World!\n";
    }
};

extern "C" ITest * testFactory() {
    return new Test{};
}

我对此所做的研究表明,这应该至少在 windows 和 linux 上跨工具链可靠地工作。即使事实并非如此,该系统旨在在一个工具链上进行快速原型设计,并有机会在某个地方静态地放弃插件和 link 一切。到目前为止没有问题。

我的其他研究表明,extern "C" linkage 说明符对于动态 linkage 在支持相同 C ABI 的所有工具中正常工作非常重要。现在的问题是:

我想编写一些模板代码来帮助简化其中的一些操作。 但是 extern "C" 与模板语法的组合似乎不起作用。我为 Google 提供帮助的努力因有关已弃用 extern template 的文章而受阻。我想给它一个抽象接口的名称,让它加载 dll 并解析工厂函数的符号。但正如您所见,回调的类型 (factory_pointer_t<T>) 由它们 returning 的抽象接口指针类型参数化。所以我的问题:

  1. 这个模板 using 指令真的可以编译吗? (它适用于 clang 但不是 gcc)

  2. 如果我将我的函数指针类型声明为:extern "C" typedef void* (*factory_pointer)(); 并使用 reinterpret_cast<> 显式转换 return 值,我会调用未定义的行为吗?鉴于在 DLL 中,我想保持导出的函数签名不变。

  3. 我是否有任何其他我可能没有考虑过的不涉及 void* 的既定目标的选择?

  4. 函数指针的 typedef 和 extern "C" 是否绝对必要?标准似乎拿不定主意。例如:在 2014 年标准草案的 7.5 中:“两种具有不同语言的函数类型 linkages 是不同的类型,即使它们在其他方面相同。”但在 8.3.5 第 8 段中:“return 类型、参数类型列表、ref 限定符和 cv- qualifier-seq,但不是默认的 参数 (8.3.6) 或异常规范 (15.4) 是函数类型的一部分。"

我真的很想让它在其他编译器中自动工作(即我给它一个类型并告诉它加载什么库然后我就完成了),但我想我没有看到我需要做什么.每个 class 涉及超过一行样板的任何内容,我都会认为是不好的。

因为这是我第一次尝试动态 linking(不仅仅是让构建系统为我做),我很想得到一些好的建议。

总结: 我有一个基于第一行代码的工作解决方案,它只能在一个编译器中工作。我知道这可能是错误的,或者至少不是普遍支持的语法。我该如何正确操作?

  1. Should this template using directive actually compile? (It does on clang but not gcc)

我不确定。该标准说函数类型、函数名称和变量名称具有语言链接,但没有说明是否可以使用别名模板来生成此类函数类型。

EDG 也拒绝它,但 Clang 允许它可能是正确的。

编辑:我找不到 http://open-std.org/JTC1/SC22/WG21/docs/cwg_closed.html#1463 指出 [temp]/4 说

A template, a template explicit specialization (14.7.3), and a class template partial specialization shall not have C linkage.

所以从技术上讲,Clang 接受它是错误的,但是 Evolution Working Group 将考虑是否应该允许它。

  1. If I declare my function pointer type as: extern "C" typedef void* (*factory_pointer)(); and cast the return value explicitly with a reinterpret_cast<>, will I be invoking undefined behavior?

(编辑:我最初在这里说是,因为我看错了问题)

不,但是 static_cast 会比 reinterpret_cast 更好。

  1. Do I have any other options I might not have considered for accomplishing my stated goal that don't involve void*?

去掉所有的C语言链接。我认为这对您的情况没有必要或有用。

  1. Is a typedef with extern "C" for the function pointer strictly necessary? The standard can't seem to make up its mind. It says for example: in 7.5 of the 2014 draft standard: "Two function types with different language linkages are distinct types even if they are otherwise identical." but in 8.3.5 paragraph 8: "The return type, the parameter-type-list, the ref-qualifier, and the cv-qualifier-seq, but not the default arguments (8.3.6) or the exception specification (15.4), are part of the function type."

这并没有说明任何关于语言链接的内容,因此与 7.5 中非常明确的声明并不矛盾,该声明是类型的一部分。

另请注意:

[expr.call]/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 (7.5).

这是未定义的,因为如果例如 C 语言链接暗示与 C++ 语言链接不同的调用约定,它将不起作用。

但是大多数编译器都会做正确的事情,因为它们实际上并没有对 C 和 C++ 函数使用不同的调用约定,而且大多数甚至没有执行 C 语言链接的函数类型不同的规则。参见例如https://gcc.gnu.org/bugzilla/show_bug.cgi?id=2316

你绝对可以做到这一点(归功于 Potatoswatter for spotting this ability in his answer here):

extern "C" {
    template <typename T>
    class factory_pointer_def {
        typedef T* (*type)();
    };
}

可以结合

template <typename T>
using factory_pointer_t = typename factory_pointer_def<T>::type;

也可以这样写

extern "C" {
    template <typename T>
    using factory_pointer_t = T* (*)();
}

并获得预期的效果,尽管标准没有给出示例。然而,标准实际上说的是

A C language linkage is ignored in determining the language linkage of the names of class members and the function type of class member functions.

alias-template两者都不是,所以C语言链接不会被忽略。

所以你需要改变的就是使用extern "C"的块形式。


正如 Jonathan 指出的那样,提供模板 C 语言链接是非法的。但是

In a linkage-specification, the specified language linkage applies to the function types of all function declarators, function names with external linkage, and variable names with external linkage declared within the linkage-specification.

语言链接不适用于模板 class 或模板别名。仅针对函数指针声明中的函数类型。

因此,上述任何在 extern "C" 块中使用模板的代码都是完全合法的。