C++ 模板类的声明与实现分离问题(模板实例化)

c++在写模版函数时(template<class T>之类的),头文件不能与cpp文件分离。这就意味者,你头文件定义的含模版的地方必须在头文件中实现,没用模版定义的地方可以放在cpp中实现。

否则,将产生错误信息:

严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK2019 无法解析的外部符号 "public: void __thiscall XXX

有什么办法可以是西安模板类的声明与实现分离(模板实例化)问题吗?

 

模板实例化有什么好处?

  1. 减少编译时间。
  2. 使得类定义与实现分离。

减少编译时间

一般来说,如果你的项目没有混合使用 C 和 C++ 语言,那么你使用 .h 和 .cpp 是没有问题的。否则你将C和C++的头文件进行分离,因为通常我们把C和C++分离编译,再统一链接。

  • 函数经过编译系统的翻译成汇编,函数名对应着汇编标号。 因为C编译函数名与得到的汇编代号基本一样,如:fun()=>_fun, main=>_main ;但是C++中函数名与得到的汇编代号有比较大的差别。 如:由于函数重载,函数名一样,但汇编代号绝对不能一样。
  • 为了区分,编译器会把函数名和参数类型合在一起作为汇编代号, 这样就解决了重载问题。具体如何把函数名和参数类型合在一起, 要看编译器的帮助说明了。
  • 这样一来,如果C++调用C,如fun(),则调用名就不是C的翻译结果_fun, 而是带有参数信息的一个名字,因此就不能调用到fun(),为了解决 这个问题,加上extern "C"表示该函数的调用规则是C的规则,则调用 时就不使用C++规则的带有参数信息的名字,而是_fun,从而达到调用 C函数的目的。
//header.hpp

#pragma once

extern "C"
{
#include "header.h" // 真正的 C header
}

好的,说会正事 ~

//foo.hpp (.h和.hpp就如同.c和.cpp似的)

template<typename T>
struct Foo {
    void f();
};
//foo.cpp

template<typename T>
void Foo<T>::f() {}

template class Foo<T1>;
template class Foo<T2>;

在标头中声明实例化:

extern template class A<int>;

并在一个源文件中定义它:

template class A<int>;

现在它只会被实例化一次,而不是每个翻译单元,这可能会加快速度。

首先需要知道的是把模版类的定义和实现分开写了,编译将会出错。

C++中每一个对象所占用的空间大小,是在编译的时候就确定的,在模板类没有真正的被使用之前,编译器是无法知道,模板类中使用模板类型的对象的所占用的空间的大小的。只有模板被真正使用的时候,编译器才知道,模板套用的是什么类型,应该分配多少空间。这也就是模板类为什么只是称之为模板,而不是泛型的缘故。

既然是在编译的时候,根据套用的不同类型进行编译,那么,套用不同类型的模板类实际上就是两个不同的类型,也就是说,stack<int>和stack<char>是两个不同的数据类型,他们共同的成员函数也不是同一个函数,只不过具有相似的功能罢了。

如上图所示,很简短的六行代码,用的是STL里面的stack,stack<int>和stack<char>的默认构造函数和push函数的入口地址是不一样的,而不同的stack<int>对象相同的函数入口地址是一样的,这个也反映了模板类在套用不同类型以后,会被编译出不同代码的现象。

所以模板类的实现,脱离具体的使用,是无法单独的编译的;把声明和实现分开的做法也是不可取的,必须把实现全部写在头文件里面。为了清晰,实现可以不写在class后面的花括号里面,可以写在class的外面。

 

实现分离

显式实例化允许您创建模板化类或函数的实例化,而无需在代码中实际使用它。因为在创建使用模板进行分发的库(.lib)文件时这很有用,所以未将实例化的模板定义放入对象(.obj)文件中。

(例如,libstdc ++包含std::basic_string<char,char_traits<char>,allocator<char> >(是std::string)的显式实例化,所以每次使用函数时std::string,都不需要将相同的函数代码复制到对象。编译器只需要将它们引用(链接)到libstdc ++。)

 

显示实例化时,当你是只有一个 cpp 的情况.可能没有什么问题。但 如果有多个 cpp 文件再使用这个模版, 你必须把它放在头文件里, 然后每个 cpp 都要 #include 这个头文件. 显示实例化之后头文件里只需要声明, 然后在其中一个 cpp 里面实现并显示实例化, 其它的 cpp 就可以直接用了.
 

在 C++ 中定义普通的类的时候,比较清晰和普遍的做法是在 .h 文件中声明类的接口,如果有 inline 函数也一并放在 .h 中的 class 关键字中。然后在 .cpp 文件中实现 .h 中声明的接口。
在定义模板类时,如果要将接口与实现分离会略有不同。如果把模板类的实现像普通类一样放在 .cpp 文件中链接器会报错。
有两个方法可以实现模板类的接口和实现在文件中的分离:

一个前提

“类模板的成员函数是一个普通函数。但是类模板的每个实例都有其自己版本的成员函数。因此类模板的成员函数具有和模板相同的模板参数。因此定义在类模板之外的成员函数就必须以关键字 template 开始,后接类模板参数列表。”
——《C++ Primer》中文版,第五版,P585

使用 .tpp 文件实现类模板的接口与实现的文件分离

比如说有这样一个模板类,这是它的接口:

template <typename Node>
class TestTemplate{
public:

  TestTemplate(Node node):
  data(node) { }

  Node data;

  void print();
};

这是它的实现:

template <typename node>
void TestTemplate<node>::print(){
    std::cout << "TestTemplate " << data << std::endl;
}

如果把它们分别放在 .h 和 .cpp 文件中,链接器会报错,提示找不到实现。

在 .h 文件中模板类的实现下加这一句:

#include "TestTemplate.tpp"

然后把实现放在名为 TestTemplate.tpp 文件中,即可。

使用显式声明实现类模板的接口与实现的文件分离

假设上面那个类的接口与实现分别放在了 .h 和 .cpp 文件中。然后在 .cpp 文件中显式的声明要使用的模板类实例,比如:

template class TestTemplate<int>;

然后,使用 TestTemplate<int> 也可以通过编译链接,但是只能使用已经显式声明的模板类实例。比如如果还要使用 TestTemplate<float>,就要这样:

template class TestTemplate<int>;
template class TestTemplate<float>;

就是说只能只用已经显式声明过的模板类实例。

 

如果您定义了一个模板类,您只想使用几个显式类型。

将模板声明放在头文件中就像普通类一样。

将模板定义放在源文件中,就像普通类一样。

然后,在源文件的末尾,显式地仅实例化您想要可用的版本。

 

我的实验:

//StringAdapter.h

#pragma once
#include <iostream>
#include <cstring>
using namespace std;

template<typename T>
class StringAdapter
{
public:
	StringAdapter(T* data);
	void doAdapterStuff();
private:
	T* m_data;
};

#include "StringAdapter.tpp"


// StringAdapter.tpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data) : m_data(data)
{
	cout << m_data << endl;
}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
	cout << "hello" << endl;
}

// main.cpp
#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{

	StringAdapter<const char>  x("hi There");
	x.doAdapterStuff();
}

 

stackoverflow 中愚蠢的例子Silly example:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

source:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

Main:

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}

 

一些有趣的小知识

《C++ Template》第六章讲过这个问题
组织模板代码有三种方式
1.包含模型(常规写法 将实现写在头文件中)
2.显式实例化(实现写在cpp文件中,使用template class语法进行显式实例化)
3.分离模型(使用C++ export关键字声明导出)

第三种方式理论最优,但是实际从C++标准提出之后主流编译器没有支持过,并且在最新的C++11标准中已经废除此特性,export关键字保留待用。
那么实际上能够使用的实现分离也就只有显式实例化

比较有意思的是,《C++ Template》书中作者建议代码为分离模型做准备,等待编译器支持之后替换,没想到最终这个特性被C++标准废弃了。

参考:https://www.zhihu.com/question/20630104

           https://bbs.csdn.net/topics/380250382

          http://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file

          https://stackoverflow.com/questions/2351148/explicit-instantiation-when-is-it-used

          google搜索:Explicit template instantiation benefits

 

  • 32
    点赞
  • 101
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值