细谈 C++ 类模板的分离式编译:类模板究竟要不要接口与实现分离

版权声明:本文为博主原创文章,未经博主允许不得转载哦 (ÒωÓױ) https://blog.csdn.net/u012814856/article/details/84645963

一、引言

只要是接触了 C++ 有一定时间的程序员,都会记住这么一个不成文的规定:

类模板的声明与实现最好放在同一个头文件中

这或许是来源于某次错误尝试的下意识的修改,又或许是简单搜索了下 C++ 类模板编译报错的原因,看到了满篇的诸如 “为什么 C++ 编译器不能支持对模板的分离式编译” 的博客,久而久之,就留下了这么一个印象。

那么实际上,如果你简单的记为 “C++ 编译器是不支持对模板的分离式编译的”,这样又有点以偏概全。那么,最准确的说法是什么呢?C++ 到底支不支持对模板的分离式编译的呢?让我来引用 《Data Structures And Algorithms Analysis In cpp》书中 1.6.5 节中的一段话:

Like regular classes, class templates can be implemented either entirely in theire declarations, or we can separate the interface from the implementation.
However, compiler support for separate compilation of templates historically has been weak and platform specific.

简单翻译下:

就像类一样,类模板是可以将其实现与声明放在一起的,或者也可以将接口与实现分离。
但是呢,编译器由于历史原因对于分离式编译的支持非常弱,并且因平台的不同支持力度有所不同。

那么接下来,在这一篇博客中,我们首先来探讨下为何类模板分离式编译会出错,以及我们会照着书上的例子,去解决分离式编译的报错问题;然后,我们再来分析下,类模板究竟是分离式编译好,还是放在一个头文件中一起编译好。

ps: 本篇博客大量参考了《Data Structure And Algorithm Analysis In C++》书中的解释和代码。

ps: 为了方便大家查看,我已经将本博客的实验代码上传到了 GitHub,地址如下:
wangying2016/Seperate_Compilation_of_Class_Template

二、实现模板的分离式编译

在这一节中,我们就像编写一个普通的类一样,实现一个 MemoryCell 类的分离式编译,也就是将接口与实现分离,声明放到 MemoryCell.h 文件中,实现放到 MemoryCell.cpp 文件中,然后再编写一个 test.cpp 文件,看看编译会不会有问题。

1. 先来尝试一下

Memory.h

#ifndef MEMORY_CELL_H
#define MEMORY_CELL_H
/**
 * A class for simulating a memeory cell.
 */

template <typename Object>
class MemoryCell
{
public:
  explicit MemoryCell(const Object & initialValue = Object{});
  const Object & read() const;
  void write(const Object & x);

private:
  Object storedValue;
};

#endif

MemoryCell.cpp

#include "MemoryCell.h"

/**
 * Construct the MemoryCell with initialValue.
 */
template <typename Object>
MemoryCell<Object>::MemoryCell(const Object & initialValue)
  : storedValue{ initialValue }
{

}

/**
 * Return the stored value.
 */
template <typename Object>
const Object & MemoryCell<Object>::read() const
{
  return storedValue;
}

/**
 * Store x.
 */
template <typename Object>
void MemoryCell<Object>::write(const Object & x)
{
  storedValue = x;
}

可以看到,我们这里就像编写一个简单的类一样,将声明放到了 MemoryCell.h 中,并为 MemoryCell 这个类模板声明了构造、read 和 write 函数,接下来,我们继续写一个 test.cpp 用来调用 MemoryCell 类模板。

test.cpp

#include "MemoryCell.h"
#include <iostream>
#include <cstdlib>
using namespace std;

int main()
{
  MemoryCell<int> m1;
  MemoryCell<double> m2{ 3.14 };

  m1.write(37);
  m2.write(m2.read() * 2);

  cout << m1.read() << endl;
  cout << m2.read() << endl;
  system("pause");
  return 0;
}

test.cpp 中的内容非常简单,使用了 MemoryCell<int>MemoryCell<double> 两种类型。接下来,让我们来编译一下它(环境是 VS 2017)。
1

报错了,让我们来仔细看看报错信息。简单来说,就是在 test.obj 链接的时候,想要找到 MemoryCell<int> 以及 MemoryCell<double> 相关类型的函数的时候,发现找不到,所以“无法解析的外部符号”。

也就是说,如果我们以普通类的写法分离接口与实现,就会出现类模板相关的函数找不到的问题。

2. 无法解析的外部符号

那让我们来仔细思考下,为什么会出现无法解析的外部符号的问题。这里,我还是引用《Data Structures And Algorithms In Cpp》书中的一段话(来自附录A,P616):

Even with this, the issue now becomes how to organize the class template declaration and the member function template definitions. The main problem is that the implementations in Figure A.2 are not actually funtions; they are simply templates awaiting expasion. But they are not even expanded when the MemoryCell template is instantiated. Each member function template is expanded only when it is invoked.

这段话将为什么会出现“无法解析的外部符号”的原因解释的非常透彻,我结合着我给出的代码简单解释下:

主要的问题就是,在我们 MemoryCell.cpp 文件中的代码并非实际上的函数,他们只是等待着扩展的模板而已。只有在 MemoryCell 模板被实例化了之后这些函数才会被扩展,也就是说,只有在调用的时候,成员函数模板才会被扩展开来。

那么问题的原因也就很清晰了,就是我们上面的代码,并没有类模板的实例化的过程。所以在编译器链接的时候,发现根本找不到 Memory<int> 以及 Memory<double> 模板的实例化的地方,所以就出现了这样的问题。

那么解决这个问题的方法也是很简单的,我们给它加上模板的实例化的代码就行了。

3. 让我们再来试一次

让我们新增一个文件 MemoryExpand.cpp,在里面进行模板的实例化操作。

MemoryExpand.cpp

#include "MemoryCell.cpp"

template class MemoryCell<int>;
template class MemoryCell<double>;

尤其注意的是,这里包含的是 MemoryCell.cpp 文件。这下,我们就可以成功编译运行出来结果了:

2

为什么呢?

我相信你一定有疑问,没事,我们一起来思考下。

就像我之前提到过的,实际上 MemoryCell.cpp 里面并不是实际上的函数,而是一个个模板。实际上你就可以将其认作是一个声明一样的东西,这里我们 #include 了 MemoryCell.cpp 文件而不是 MemoryCell.h 头文件,是因为只有 MemoryCell.cpp 文件中有类模板实例化所需要的全部内容。MemoryCell.cpp 里面的模板,就像是万事俱备只欠东风一样,这个东风,就是类模板的实例化,让其全部活过来。

这样,我们就完成了类模板的接口与实现分离的分离式编译。最主要的就是要特别进行一下类模板的实例化操作,不然会出现找不到函数的问题。

ps: 这里我推荐一篇博客,如果你对于为什么会报错还是有所疑问的话,可以点开下面的链接,我觉得写得还是很不错的:
为什么C++编译器不能支持对模板的分离式编译

三、实现模板的一体化编译

相对应于分离式编译,我们也可以将类模板的接口与实现都放在一个头文件中,对比起来学习,或许效果更好,这里,就让我们试试。

MemoryCell.h

#ifndef MEMORY_CELL_H
#define MEMORY_CELL_H
/**
 * A class for simulating a memeory cell.
 */

template <typename Object>
class MemoryCell
{
public:
  explicit MemoryCell(const Object & initialValue = Object{});
  const Object & read() const;
  void write(const Object & x);

private:
  Object storedValue;
};

/**
 * Construct the MemoryCell with initialValue.
 */
template <typename Object>
MemoryCell<Object>::MemoryCell(const Object & initialValue)
  : storedValue{ initialValue }
{

}

/**
 * Return the stored value.
 */
template <typename Object>
const Object & MemoryCell<Object>::read() const
{
  return storedValue;
}

/**
 * Store x.
 */
template <typename Object>
void MemoryCell<Object>::write(const Object & x)
{
  storedValue = x;
}

#endif

test.cpp

#include "MemoryCell.h"
#include <iostream>
#include <cstdlib>
using namespace std;

int main()
{
  MemoryCell<int> m1;
  MemoryCell<double> m2{ 3.14 };

  m1.write(37);
  m2.write(m2.read() * 2);

  cout << m1.read() << endl;
  cout << m2.read() << endl;
  system("pause");
  return 0;
}

这两份代码编译是没有问题的,没有分离式编译,就没有那么复杂的链接问题。MemoryCell<int>MemoryCell<double> 想要的信息在 MemoryCell.h 中都有,没有分离的 cpp 文件就没有分离的 obj 文件也就不存在链接问题了。

可见,类模板将接口与实现放在头文件中进行编译,是非常简单并且不容易出错的。

四、我们该如何选择

或许对于喜欢省事的朋友来说,管那么多干嘛,我只选择实现最简单的最不容易出错的,那么显然类模板的接口与实现放在一起是最好的选择。不过,我们还是严谨一些,认真分析一下优劣,这样能让我们的认知能够更加深刻一些。

1. 类模板接口与实现分离的优点与缺点

优点自然是逻辑清晰,不用多个每一个包含了类模板接口定义的源文件都包含一份实现的副本。而且就拿我们上面的例子来说,如果 MemoryCell 的实现发生了变化,也就是 MemoryCell.cpp 文件有改动,那么需要重新编译的就只有 MemoryCellExpand.cpp 文件。相对来说耦合度有所降低。

缺点那就太明显,甚至有些许致命,C++ 编译器对于类模板的分离式编译支持不到位,跨平台兼容问题很大。甚至解决编译问题的方法,随着平台的不同有所不同。

2. 类模板接口与实现放在一起的优点与缺点

优点自然是简单不易错,不存在跨平台兼容问题。

缺点那就是在编译的时候,多份包含了类模板头文件定义的源文件会拥有重复的类模板成员函数实现的定义。不过正因为即使是类模板的实现也都是模板,不是类,所以这也不是很大的问题。

3. 你说了那么多,那我们的选择是…

其实我们只需要随着主流走就行。作为一个 C++ 程序员,STL 的地位毋庸置疑,STL 的类模板都是接口与实现放在一起的,我们随着主流走就可以。

当然了,如果实在项目需要,可以将单个头文件按照本篇博客的方法分离出来,除了要注意下平台的兼容性,其他应该没有什么问题。

五、总结

这篇博客来自于《Data Structures And Algorithms In Cpp》一些章节内容的阅读和思考。可以说,本篇博客的探讨的结果很简单,还是推荐使用类模板的接口与实现放在头文件中的方式进行编译,但是去深入的了解下为什么,以及亲手去实践下分离式编译也并不是一件坏事。

C++ 涉及到的内容非常的多,一点一滴的小问题的思考所总结下来的经验与教训,久而久之会让你成为一个最纯正的 C++ 人。

C++ 之路,路漫漫其修远兮啊 ~~~

To be Stronger:)

猜你喜欢

转载自blog.csdn.net/u012814856/article/details/84645963