共享库:具有部分模板特化和显式模板实例化的未定义引用

Shared Library: Undefined Reference with Partial Template Specialization and Explicit Template Instantiation

比如说,有一个第三方库在头文件中有以下内容:

foo.h:

namespace tpl {
template <class T, class Enable = void>
struct foo {
  static void bar(T const&) {
    // Default implementation...
  };
};
}

在我自己的库的界面中,我应该为我自己的类型提供此 foo 的部分专业化。所以,假设我有:

xxx.h:

# include <foo.h>

namespace ml {
struct ML_GLOBAL xxx {
  // Whatever...
};
}

namespace tpl {
template <>
struct ML_GLOBAL foo<::ml::xxx> {
  static void bar(::ml::xxx const&);
};
}

其中 ML_GLOBAL 是特定于编译器的 可见性属性 以确保符号可用于动态链接(默认情况下我的构建系统隐藏所有生成的共享库中的符号)。

现在,我不想公开 bar 的实现,所以我使用 显式模板实例化 :

xxx.cpp:

# include "xxx.h"

namespace tpl {
void foo<::ml::xxx>::bar(::ml::xxx const&) {
  // My implementation...
}

extern template struct foo<::ml::xxx>;
}

当在某些消费者应用程序中实际使用此 tpl::foo<::ml::xxx>::bar 函数时(也链接了我的共享库),我收到 undefined reference 错误到 tpl::foo<::ml::xxx, void>::bar 符号。事实上,生成的共享库上的 运行 nm -CD 没有显示 tpl::foo<::ml::xxx, void> 符号的痕迹。

到目前为止,我尝试过的是 ML_GLOBAL 放置位置的不同组合(例如,在显式模板实例化本身上,GCC 明显抱怨与 Clang 不同的地方)和 with/without 第二个模板参数 void.

问题是,这是否与原始定义由于来自第三方库而没有附加可见性属性 (ML_GLOBAL) 的事实有关,还是我实际上在这里遗漏了什么?如果我没有遗漏任何东西,那么我真的被迫在这种情况下公开我的实现吗? [... *咳咳* 说实话看起来更像是编译器的缺陷 *咳咳* ...]

原来是虚惊一场。尽管如此,我还是花了几个小时才记起为什么这个符号对消费者来说可能是看不见的。这真的很微不足道,但我想将它张贴在这里,以供将来碰巧具有相同设置的访问者使用。基本上,如果您使用 linker 脚本 [1] or a (pure) version script [2](使用 --version-script linker 选项指定),那么不要忘记为那些 tpl::foo* 第三方符号(或您的情况下的任何符号)设置 global 可见性。就我而言,我最初有以下内容:

{
global:
  extern "C++" {
    ml::*;
    typeinfo*for?ml::*;
    vtable*for?ml::*;
  };

local:
  extern "C++" {
    *;
  };
};

我显然必须更改为

{
global:
  extern "C++" {
    tpl::foo*;
    ml::*;
    typeinfo*for?ml::*;
    vtable*for?ml::*;
  };

local:
  extern "C++" {
    *;
  };
};

为了link一切正常并得到预期的结果。

希望这对您有所帮助和问候。

奖金


好奇的 reader 可能会问,"Why the hell are you combining explicit visibility attributes and a linker/version script to control visibility of symbols when there are already the -fvisibility=hidden and -fvisibility-inlines-hidden options which are supposed to do just that?"。

答案是它们当然可以,而且我确实使用它们来构建我的共享库。但是,有一个陷阱。通常的做法是 link 共享库静态使用的一些内部库(私下)(进入该库),主要是为了完全隐藏此类依赖性(不过请记住,共享库附带的头文件图书馆也应该适当地设计来实现这一点)。好处很明显:干净且可控的 ABI 并减少共享库使用者的编译时间。

以 Boost 为例,它是此类用例最广泛的候选者。将来自 Boost 的所有高度模板化代码私下封装到您的共享库中,并从 ABI 中消除任何 Boost 符号,将大大减少接口污染和共享库使用者的编译时间,这还不包括您的软件组件看起来也将是专业开发的事实。

无论如何,事实证明,除非您想 link 到您的共享库中的那些静态库本身也是使用 -fvisibility=hidden-fvisibility-inlines-hidden 选项构建的(这是多么荒谬的期望,因为没有人会默认分发带有隐藏接口符号的静态库,因为这违背了他们的目的),他们的符号将不可避免地仍然可见(例如,通过nm -CD <shared-library>) 不管您是使用这些选项构建共享库本身。也就是说,在这种情况下,您有两种选择来解决它:

  1. 使用 -fvisibility=hidden-fvisibility-inlines-hidden 选项手动重建这些静态库(您的共享库依赖项),鉴于它们可能来自第三方,显然并不总是 possible/practical。
  2. 使用 linker/version 脚本(就像上面所做的那样)在 link 时间提供,以指示 linker 从您的共享中强制 export/hide 正确的符号图书馆。