编写库时,是否应将可见性/导出宏应用于模板

Should visibility/export macros be applied to templates when writing a library?

本文关键字:应用于 可见性 是否      更新时间:2023-10-16

在构建C++DLL或共享库时,__attribute__((__visibility__("default")))__declspec(dllexport)经常通过宏附加到那些应该提供给库使用者的具体符号(类、函数等(上,其他符号默认为具有内部可见性。

但是,对于内联函数或模板应该做些什么呢?

似乎对于内联函数,答案应该是不需要注释。如果定义内联函数的头的使用者实际上内联了函数,那么就不需要符号了。如果使用者发出一个越界的定义,那还是可以的。唯一的问题是DLL内部和每个使用者库内部的内联函数的定义可能不同。因此,如果您希望可靠地比较内联函数的地址,您可能会遇到一些麻烦,但这似乎很粗略。

鉴于这一论点,由于模板主体通常对消费TU完全可见,因此同样的逻辑也适用。

我觉得这里可能有一些关于"外部模板"和显式实例化的微妙之处。

是否有人对可见性属性应如何遵守内联函数和模板有具体的指导?

内联函数在外部不可见(没有链接,IIRC(,因此无法从DLL导出。如果它们是公共的,那么它们会完全写入库的头文件中,每个用户都会重新编译它

正如您在问题中所说,由于内联代码在使用该库的每个模块中都会重新编译,因此对于该库的未来版本,可能会出现问题。

我对共享库中的内联函数的建议是,它们应该只用于真正琐碎的任务或绝对通用的函数。请注意,将公共内联函数转换为非内联函数是破坏ABI的更改。

例如:

  • 一个类memcpy函数。内联
  • 一个类似bswap的函数。内联
  • 类构造函数。不要内联!即使它现在什么都不做,你也可能想在未来版本的库中做点什么。在库中编写一个非内联的空构造函数并将其导出
  • 类析构函数。不要内联!同上

实际上,一个内联函数可以有几个不同的地址,这一点并不重要。

关于外部模板显式实例化,请注意,它们可以用于从库导出模板。如果模板实例化仅限于一组特定的情况,您甚至可以避免将模板代码复制到头文件中。

注1:在下面的例子中,我将使用一个简单的函数模板,但类模板的工作原理完全相同。注2:我使用的是GCC语法。MSC代码是相似的,我想你已经知道区别了(我没有MSC编译器来测试(。

示例1

public_foo.h

template<int N> int foo(int x); //no-instantiable template

shared_foo.cpp

#include "public_foo.h"
//Instantiate and export
template __attribute__ ((visibility("default")))
int foo<1>(int x);
template __attribute__ ((visibility("default")))
int foo<2>(int x);

程序.cpp

#include "public_foo.h"
int main()
{
    foo<1>(42); //ok!
    foo<2>(42); //ok!
    foo<3>(42); //Linker error! this is not exported and not instantiable
}

相反,如果您的模板应该是可自由实例化的,但您希望它以特定的方式频繁使用,则可以从库中导出这些模板。想象一下std::basic_string:它最有可能被用作std::basic_string<char>std::basic_string<wchar_t>,但不太可能用作std::basic_string<float>

示例2

public_foo.h

template<int N> int foo(int x)
{
    return N*x;
}
//Do not instantiate these ones: they are exported from the library
extern template int foo<1>(int x);
extern template int foo<2>(int x);

shared_foo.cpp

#include "public_foo.h"
//Instantiate and export
template __attribute__ ((visibility("default")))
int foo<1>(int x);
template __attribute__ ((visibility("default")))
int foo<2>(int x);

程序.cpp

#include "public_foo.h"
int main()
{
    foo<1>(42); //ok, from library
    foo<2>(42); //ok, from library
    foo<3>(42); //ok, just instantiated
}

我认为您要问的问题可以归结为以下两个问题:

何时需要显式导出带有__attribute__((__visibility__("default")))的符号

这里的规则是,如果方法的实现是共享库内部的(通常,它在.cpp文件中,在external.h中有一个声明(,则在编译共享库时,需要将该符号的声明标记为外部可见。如果您未能做到这一点,那么在外部库中编译任何使用该方法的东西都会在链接时抱怨——所以这个问题很容易在测试中发现。

这是假设您还添加了-fvisibility=hidden来默认内化所有符号。

__attribute__((__visibility__("default")))添加到具有外部可见定义的内联函数或模板函数中有害吗

事实并非如此,因为默认可见性的一个常见用例是标记整个类以进行导出,这可能包括outline和inline方法。在这种情况下,内联方法将是内联的(在任何使用它的翻译单元中(,并且不生成外部符号。如果您稍后选择勾勒该方法的轮廓,则符号可见性将生效。

对于模板,显式模板实例化只是一种允许您为模板方法创建大纲定义的机制,并且与普通内联/大纲方法应用相同的规则。