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<int>或者TreeNode<char>)。因此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++的模板生成實例化相關。我覺得自己還是對模板的生成、實例化等內容不熟悉,不然這個問題還是很容易解決的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章