模板类专门用于lib内部和外部

时间:2011-07-29 07:13:37

标签: c++ templates static-libraries explicit-specialization

考虑这个合成的例子。我的Visual Studio 2010解决方案中有两个本机C ++项目。一个是控制台exe,另一个是lib。

lib中有两个文件:

// TImage.h

template<class T> class TImage
{
public:
  TImage()
  {
#ifndef _LIB
    std::cout << "Created (main), ";
#else
    std::cout << "Created (lib), ";
#endif
    std::cout << sizeof(TImage<T>) << std::endl;
  }

#ifdef _LIB
  T c[10];
#endif
};

void CreateImageChar();
void CreateImageInt();

// TImage.cpp

void CreateImageChar()
{
  TImage<char> image;
  std::cout << sizeof(TImage<char>) << std::endl;
}
void CreateImageInt()
{
  TImage<int> image;
  std::cout << sizeof(TImage<int>) << std::endl;
}

exe文件中的一个文件:

// main.cpp

int _tmain(int argc, _TCHAR* argv[])
{
  TImage<char> image;
  std::cout << sizeof(TImage<char>) << std::endl;

  CreateImageChar();
  CreateImageInt();

  return 0;
}

我知道,我实际上不应该这样做,但这只是为了理解正在发生的事情。那就是,会发生什么:

// cout:
Created (main), 1
1
Created (main), 1
10
Created (lib), 40
40

那么究竟是怎么发生的,那个链接器用exe的版本TImage<char>覆盖了lib的TImage<char>版本?但由于没有exe版本的TImage<int>,它会保留lib的版本TImage<int>?这种行为是否标准化,如果是这样,我在哪里可以找到描述?

更新:下面给出的效果说明是正确的,谢谢。但问题是“这究竟是怎么发生的”?我希望得到一些链接器错误,如“乘法定义的符号”。所以最合适的答案来自Antonio Pérez's reply

6 个答案:

答案 0 :(得分:2)

模板代码会创建重复的对象代码。

编译器会复制实例模板时提供的类型的模板代码。因此,在编译TImage.cpp时,您会获得模板的两个版本的对象代码,一个用于 char ,另一个用于TImage.o中的 int 。然后编译main.cpp,您就会在main.o中获得 char 模板的新版本。然后链接器恰好使用main.o 始终中的那个。

这解释了为什么您的输出产生'Created'行。但是看到第3,4行关于物体尺寸的不匹配有点令人费解:

Created (main), 1
10

这是由于编译器在编译时解析sizeof运算符。

答案 1 :(得分:1)

我假设您正在构建静态库,因为代码中没有__decelspec(dllexport)extern "C"。这里发生的是以下内容。编译器为您的lib创建TImage<char>TImage<int>的实例。它还为您的可执行文件创建一个实例。当链接器加入静态库和可执行文件的对象时,重复的代码将被删除。请注意,静态库以类似的目标代码链接,因此如果您创建一个大的可执行文件或多个静态库和一个可执行文件,它们没有任何区别。如果要构建一个可执行文件,结果将取决于对象链接的顺序;又名“未定义”。

如果将库更改为DLL,则行为会发生变化。由于您调用DLL的边界,因此每个都需要TImage<char>的副本。在大多数情况下,DLL的行为与您期望的库工作方式相同。静态库通常只是一种便利,因此您无需将代码放入项目中。

注意:这仅适用于Windows。在POSIX系统上* .a文件的行为类似于* .so文件,这为编译器开发人员带来了一些麻烦。

编辑:永远不要在DLL边界上传递TIm​​age类。这将确保崩溃。这就是为什么Microsoft的std :: string实现在混合调试和发布版本时崩溃的原因。它们完全按照NDEBUG宏的方式执行。

答案 2 :(得分:0)

内存布局是一个编译时的概念;它与链接器无关。 main函数认为TImage小于CreateImage...函数,因为它是使用不同版本的TImage编译的。

如果在标题中将CreateImage...函数定义为内联函数,它们将成为main.cpp编译单元的一部分,因此将报告与main报告相同的大小特征。

这也与模板和实例化时没有任何关系。如果TImage是一个普通的类,你会观察到相同的行为。

编辑:我刚注意到第三行cout不包含“Created(lib),10”。假设它不是拼写错误,我怀疑发生了什么CreateImageChar没有内联对构造函数的调用,因此使用的是main.cpp的版本。

答案 3 :(得分:0)

当您使用模板时,编译器将始终实例化您的模板 - 如果定义可用。

这意味着,它为所需的特化生成所需的函数,方法等,并将它们放在目标文件中。这就是为什么您需要为您正在使用的特定专业化提供定义(通常在头文件中)或现有实例化(例如在另一个目标文件或库中)的原因。

现在,在链接时,可能会出现通常不允许的情况:每个类/函数/方法有多个定义。对于模板,这是特别允许的,编译器将为您选择一个定义。这就是你的情况和你称之为“重写”的事情。

答案 4 :(得分:0)

模板在编译期间创建类的重复(即空间)。因此,当您使用太多模板时,智能编译器会尝试根据模板的参数化来优化它们。

答案 5 :(得分:0)

在您的库中,您有一个TImage在构造时打印“lib”,并包含一个T数组。其中有两个,一个用于int,另一个用于char

在main中,你有一个TImage在构造上打印“main”,并且不包含T的数组。在这种情况下只有char版本;因为你从不要求创建int版本。

当你去链接时,链接器选择两个TImage<char>构造函数中的一个作为官方构造函数;碰巧选择了main的版本。这就是为什么你的第三行打印“main”而不是“lib”;因为您正在调用该版本的构造函数。通常,您不关心构造函数的哪个版本被调用...它们必须全部相同,但您违反了该要求。

这是重要的部分:你的代码现在已经被破坏了。在您的库函数中,您希望在char 中看到TImage<char>的数组,但构造函数从不创建它。此外,想象一下您是否在new TImage<char>内{ {1}},并将指针传递给库中的一个函数,该函数将被删除。 main分配一个字节的空间,库函数试图释放十个。或者,如果您的main方法返回了它创建的CreateImageChar ... TImage<char>将在堆栈上为返回值分配一个字节,main将为其填充10个字节数据的。等等。