DLL-导出模板基类的静态成员

Posted

技术标签:

【中文标题】DLL-导出模板基类的静态成员【英文标题】:DLL-Exporting static members of template base class 【发布时间】:2011-10-21 11:40:20 【问题描述】:

在 DLL 中,我有一个带有模板基类的导出非模板类。这个模板基类有一个静态成员变量。我在一个可执行文件中使用静态基成员,该文件通过导出的非模板类链接到 DLL。

在许多情况下,我会收到未解决的外部符号或有关不一致链接的投诉。我找到了一种可行的方案,但它似乎很笨拙,所以我想知道是否有更好的方法,以及这种更好的方法是否也可能指向 VS2010 SP1 的 C++ 编译器/链接器的缺陷。

这是我可以提取的 DLL 的最小场景 - 我认为我不能在不破坏场景的情况下删除这里的任何内容。

// Header file
template<typename T>
class _MYDLL_EXPORTS TBaseClass
  
  public:
    static const double g_initial_value;
  ;

class _MYDLL_EXPORTS MyClass : public TBaseClass<MyClass>
      
  ;

// Kludge: use this code only when building the DLL, not when including
// from the DLL's client
#ifdef _MYDLL
  template<typename T>
  const double TBaseClass<T>::g_initial_value = 1e-5;
#endif


// CPP file
#include "header.h"
// Explicit instantiation of the template for the correct parameter.
template class TBaseClass<MyClass>;

然后是DLL的用户

#include <header.h>  
#include <iostream>
int main(void) 
 MyClass c;
 std::cout << c.g_initial_value;
return 0;

【问题讨论】:

您可以用 C++ 编写 DLL,但导出的接口应始终与 C 兼容。其他任何事情都是自找麻烦。您的设计很脆弱,并且可能会因库和客户端之间编译器选项的最细微差异而中断。 @Ben Voigt - 虽然我同意您关于通用库的观点,但在这种情况下,库和客户端都在我的完全控制之下,并且它们保证使用相同的构建编译器和设置通过各种机制。 maco _MYDLL_EXPORTS 根据包含的位置扩展为 _declspec(dllexport) 或 _declspec(dllimport) - 根据我的经验,这是一种非常常见的模式,适用于较大的原生 C++ win32 应用程序。 您的解决方案已经正确。如果是模板,链接器会自动消除重复项。此外,VC++ 链接器可能更喜欢 TBaseClass::g_initial_value 的 dllimport 版本,但事实并非如此。通过使用 #ifdef _MYDLL 你可以帮助它。您可以向 MS Connect 提交错误,但他们倾向于关闭诸如“无法修复”之类的问题。 【参考方案1】:

在 C++ 中,当一个普通类有一个静态成员时,它应该在头文件中声明,但在源文件中实例化。否则会导致创建过多的静态类成员实例。

模板有点类似,除了编译器对模板有一些魔力,而对非模板没有。具体来说,它在构建的链接阶段神奇地消除了模板实例化的重复实例。

这是您问题的根源:_MYDLL 部分中的内容由包含此模板的每个源文件自动实例化,并且还生成 TBaseClass 对象。然后链接器会自动消除重复。

问题是,你有两个链接:DLL 链接和客户端链接。两者都将创建 TBaseClass 实例化,并且都将创建那些 g_initial_value 对象。

要解决这个问题:将 _MYDLL 条件条件中的内容移动到 CPP 文件中,这样客户端就不会获得构建实例本身的指令。

【讨论】:

我无法将静态模板成员的定义移动到 CPP 文件中,因为客户端将基模板类用于某些目的(对于实例/类型不是在 DLL 中创建的)。或者我可以吗?在客户端重新实现专门的静态会不会那么麻烦? 并且必须为每个专业重复一次?我更喜欢你原来的方法。您可以做的是使用附加条件来确保静态成员定义仅对执行显式实例化的源文件可见。 @NicolaMusatti - 我对这个建议很感兴趣,会尝试一些与此相关的想法。 C++ 令人讨厌,因为它将声明与实现分开。 C++ 模板绕过了这种麻烦并允许头文件中的所有内容(以换取缓慢而脆弱的重新编译),但静态成员变量无法利用。 Re: TBaseClass 的两个实例化 - 我同意会发生这种情况,但我认为它不应该因为,正如您在回答中所说,编译器应该消除模板的重复实例,包括静态的,但它没有(但仅适用于静态)。我开始认为模板和 DLL 无论如何都不会混合使用,还有其他几个原因。【参考方案2】:

您同时使用 DLL 和 EXE 中的模板类这一事实使事情变得更加混乱,但它仍然可以工作。

首先,您应该完全在头文件中实现您的模板基类。如果您不知道原因,请确保您阅读了this question 接受的答案。

现在让我们忘记模板和 DLL,考虑一个更简单的情况。假设您有 C 类,带有一个静态成员。您通常会以这种方式编写此类:

// C.h file
class C 
public:
    static const double g_initial_value;
;

// C.cpp file
const double C::g_initial_value = 1e-5;

这里没有什么奇怪或复杂的。现在考虑如果将静态声明移动到头文件会发生什么。如果只有一个源文件包含头文件,那么一切都会正常工作。但是如果两个或多个源文件包含这个头文件,那么这个静态成员会被定义多次,链接器不会这样。

同样的概念也适用于模板类。您的#ifdef _MYDLL hack 仅适用,因为从 DLL 中您仅包含此头文件一次。但是,当您从另一个源文件中包含此文件时,您将开始在 DLL 上收到链接器错误!所以我完全同意你的看法,这不是一个好的解决方案。

我认为使您的设置复杂化的一件事是您允许 DLL 和 EXE 实例化此模板基类。如果您为模板类的每个实例找到一个“所有者”,我认为您将有一个更清洁的解决方案。按照您的代码示例,让我们将 MyClass 替换为 MyDLLClass 和 MyEXEClass。然后你可以这样工作:

// dll.h
template<typename T>
class _MYDLL_EXPORTS TBaseClass
  
  public:
    static const double g_initial_value;
  ;

class _MYDLL_EXPORTS MyDLLClass : public TBaseClass<MyDLLClass>
      
  ;

// dll.cpp
#include "dll.h"

// this file "owns" MyDLLClass so the static is defined here
template<> const double TBaseClass<MyDLLClass>::g_initial_value = 1e-5;

// exe.h
#include "dll.h"

class MyEXEClass : public TBaseClass<MyEXEClass>
      
  ;

// exe.cpp
#include "exe.h"
#include <iostream>

// this file "owns" MyEXEClass so the static is defined here
template<> const double TBaseClass<MyEXEClass>::g_initial_value = 1e-5;

int main(int argc, char* argv[])

    MyDLLClass dll;
    MyEXEClass exe;

    std::cout << dll.g_initial_value;
    std::cout << exe.g_initial_value;

我希望这是有道理的。

【讨论】:

你比我更清楚地解释了问题的根源,因为我会给你+1。但是,我的笨拙的 hack 不会在 DLL 中失败!看起来在 DLL 内部,编译器确实设法正确检测到基本模板已被实例化,并且没有引入 TBaseClass 静态初始化的第二个实例。 有趣。您是对的,Microsoft C++ 编译器(VS2010)确实允许您多次定义静态符号(即使为每个符号分配了不同的值!)但链接器只看到一个并默默地接受它。但是,我也测试了 GCC,它肯定不喜欢这样,并且表现得像我上面描述的那样。我强烈建议您不要尝试寻找特定于编译器的解决方案,我认为这是一个 VS 错误。【参考方案3】:

事实上,如果基类是模板类,导出类的基类也会被导出,但反之则不然。请参考http://www.codesynthesis.com/~boris/blog/2010/01/18/dll-export-cxx-templates/

对于您的具体问题,我建议您在基本模板中定义一个静态方法,该方法返回一个感兴趣的变量(指针?)。那么在多个 dll 或 exe 中只会发生一个定义,这取决于您的库。

【讨论】:

有趣 - 看起来模板和 DLL 仍然有很多黑暗的角落。最后,我们通过将模板中的 const 静态成员移到其他地方解决了这个问题 - 并尽可能避免静态。【参考方案4】:

虽然我建议使用您当前的方法,但实际上可以通过使用旧语法从 DLL 导出模板来避免 #ifdef。所有这些都转到 DLL 的头文件中:

#pragma once

#ifdef _MYDLL
#define _MYDLL_EXPORTS __declspec(dllexport)
#else
#define _MYDLL_EXPORTS __declspec(dllimport)
#endif

template<typename T> 
class _MYDLL_EXPORTS TBaseClass // _MYDLL_EXPORTS is not needed here
 
public: 
    static double g_initial_value; 
; 

template<typename T> 
double TBaseClass<T>::g_initial_value = 1e-5; 

class MyClass;

template class _MYDLL_EXPORTS TBaseClass<MyClass>;

class _MYDLL_EXPORTS MyClass : public TBaseClass<MyClass> 
     
; 

在运行时,客户端代码中 g_initial_value 的地址位于 DLL 的地址空间内,因此它似乎可以正常工作。

【讨论】:

此解决方案也不起作用 - 只要您尝试在 DLL 之外实例化一个尚未在 DLL 内显式(或隐式)实例化的 TBaseClass,您就会收到错误 C2491 - 定义不允许使用 dllimport 静态数据成员。

以上是关于DLL-导出模板基类的静态成员的主要内容,如果未能解决你的问题,请参考以下文章

如何在调用基类的静态函数之前设置派生静态成员

让基类的方法使用继承类的静态成员变量......可能吗?

enum 不是非静态数据成员或类的基类

从基类到派生类的静态转换后找不到成员函数

C++派生类是不是可以从基类继承静态数据成员和静态成员函数?

生成导出包含 ATL::CString 成员的类的 DLL 时出现警告 C4251