C++ 模板函数与分离编译问题

前言

今天在写一个模板类的时候,涉及到模板类的成员函数定义。按一般的情况来说,类的成员函数的声明是放在头文件(.h)中,而成员函数的定义是放在相应的源文件(.cpp)中。在调用函数时,编译器只需要掌握函数的声明,具体的定义可以到所链接进来的源文件产生的目标文件(.o)中寻找。所以我写模板类的时候也照着这个原则做了,结果运行的时候报错了。接下来就来简单分析一下原因和解决方案。

问题描述

首先有一个自己写的模板类,类定义写在TreeNode.h头文件中,成员函数在头文件中只有声明:

#ifndef STUDENT_H
#define STUDENT_H
// 条件define,防止重复定义

template <class T>
class TreeNode {
public:
	typedef T value_type;
	typedef T* pointer;

    TreeNode(value_type data, pointer next);

	value_type& get_data();
	pointer get_next();
protected:
	value_type data;
	pointer next;
};
#endif

源文件TreeNode.cpp,包含成员函数的定义:

#include "TreeNode.h"

template <class T>
TreeNode<T>::TreeNode(typename TreeNode<T>::value_type data, typename TreeNode<T>::pointer next)
        : data(data), next(next) { }


template <class T>
typename TreeNode<T>::value_type& TreeNode<T>::get_data() {
	return this->data;
}

template <class T>
typename TreeNode<T>::pointer TreeNode<T>::get_next() {
	return this->next;
}

源文件main.cpp,主函数:

#include <iostream>
#include "TreeNode.h"

using namespace std;


int main() {
    TreeNode<int> node(10, nullptr);
    cout << node.get_data() << endl;
    
    return 0;
}

接着编译运行,报错,报错信息如下:

[zb test]$ make
g++ -c TreeNode.cpp -std=c++11
g++ -o main main.o TreeNode.o -std=c++11
main.o: In function `main':
main.cpp:(.text+0x1a): undefined reference to `TreeNode<int>::TreeNode(int, int*)'
main.cpp:(.text+0x26): undefined reference to `TreeNode<int>::get_data()'
collect2: error: ld returned 1 exit status
make: *** [main] Error 1

报错的意思是说找不到TreeNode<int>::TreeNode(int, int*)TreeNode<int>::get_data()函数的定义。

问题分析

一般来说,函数的声明和定义分离,是不会出现上述的问题的。这里因为是模板类的成员函数,所以就涉及到了模板定义的问题。下面引用《C++ primer》第五版P582中的原话:

  1. 当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才生成代码
  2. 通常当我们调用一个函数时,编译器只需要掌握函数的声明,类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但是成员函数的定义不必已经出现。因此可以将类定义和函数声明放在头文件中,而将普通函数和类的成员函数的定义放在源文件中。
  3. 模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义

我的理解是:

  • 模板类不是一个具体的类型,没有相关的代码,只有在对模板类进行实例化后,才会生成一个具体的类和类内部的成员函数。所以如果没有实例化,是不可能使用模板类中的成员函数的。
  • 而如果不对模板类进行显式实例化的话,那么只有在传入模板类型实参给模板类,并使用这个模板类之后,编译器才会给这个已经指定类型实参的模板类生成具体的代码。

举个例子,虽然stack<int>stack<char>使用的是同一个模板类,但是它们是两个不同的类。因此这两个具体类内部的成员(成员变量和成员函数)也是不同的。编译器会在使用stack<int>stack<char>的地方生成相应的类成员信息。

从执行流程来看,在main.cpp中通过TreeNode<int>来调用模板类的成员函数时,实际上调用的是TreeNode<int>这一个具体类的成员函数,而main.cppTreeNode.h中并没有关于这一个具体类成员函数的定义,所以链接过程中它需要在其他的.o目标文件中寻找成员函数的定义。

而在TreeNode.cpp编译成TreeNode.o目标文件时,因为内部没有使用具体模板类,也没有对模板类进行显式实例化,那么编译器对于该文件内的模板类不会进行实例化。因此TreeNode.cpp内的成员函数定义其实是无意义的,它没有被实例化,只是一个空的模板函数。而如果要在符号表中生成它的符号信息,首先它得是一个完整的函数。空的模板函数不是完整的函数,只有模板函数实例化之后才是一个真正可以调用的完整函数。因此TreeNode.o目标文件中也不会生成TreeNode<int>这一具体类中成员函数的符号信息。

那么在main.cpp调用TreeNode<int>这一具体类的成员函数之后,编译器无法在TreeNode.o目标文件中找到关于TreeNode<int>这一具体类的成员函数的符号信息。所以报错说找不到成员函数的定义。

解决方案

错误的原因是编译器没有对TreeNode.cpp中的模板成员函数进行实例化,生成一个完整的函数,因此在符号表中没有这个成员函数的符号信息。那么我们可以考虑对模板成员函数进行实例化,从而使得编译器在TreeNode.o中生成具体类的模板成员函数的定义以及符号信息。

方法1 将模板类的成员函数声明和定义都放在头文件中(推荐的做法)

这个方法也就是《C++ primer》上推荐的方法,因为main.cpp本身包含了TreeNode.h这一头文件,所以在调用TreeNode<int>的成员函数的时候,可以根据模板类和模板成员函数生成与具体类TreeNode<int>有关的定义,那么也就可以直接找到生成后TreeNode<int>的成员函数的定义。

方法2 在TreeNode.cpp中使用模板类以及相应的成员函数(隐式实例化)

这个方法会让编译器在TreeNode.cpp中对模板类隐式实例化,生成具体类和内部成员函数的定义,那么在TreeNode.o目标文件中就会有TreeNode<int>的成员函数信息了。例如在TreeNode.cpp中加入如下代码:

void hello() {
	// 使用TreeNode<int>的构造函数,会在此文件中生成TreeNode<int>的构造函数定义
	TreeNode<int> a(10, nullptr);
	// 使用TreeNode<int>的get_data(),会在此文件中生成TreeNode<int>的get_data()定义
	a.get_data();
	// 使用TreeNode<int>的get_next(),会在此文件中生成TreeNode<int>的get_next()定义
	a.get_next();
}

实测,没有报错。当然如果上面的TreeNode<int>换成TreeNode<char>,那么还是没有生成TreeNode<int>这一具体类的成员函数定义,所以main.cpp中的调用还是会报错。所以这个方法还是比较麻烦的,调用了哪个成员函数,就需要在TreeNode.cpp中先使用它。

方法3 为成员函数显式实例化

这个方法与方法2是相似的,只不过方法2是通过使用TreeNode<int>成员函数的方法来隐式实例化成员函数,而这里是直接显式实例化:

// 显式实例化TreeNode<int>的构造函数
template TreeNode<int>::TreeNode(typename TreeNode<int>::value_type data, typename TreeNode<int>::pointer next);
// 显式实例化TreeNode<int>的get_data()
template typename TreeNode<int>::value_type& TreeNode<int>::get_data();
// 显式实例化TreeNode<int>的get_next()
template typename TreeNode<int>::pointer TreeNode<int>::get_next();

实测,没有报错。同理,如果上面的TreeNode<int>换成TreeNode<char>,那么还是没有生成TreeNode<int>这一具体类的成员函数定义,所以main.cpp中的调用还是会报错。

总结

其实将这个问题分析下来,就是跟c++的分离编译c++的模板生成实例化相关。我觉得自己还是对模板的生成、实例化等内容不熟悉,不然这个问题还是很容易解决的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值