前言
今天在寫一個模板類的時候,涉及到模板類的成員函數定義。按一般的情況來說,類的成員函數的聲明是放在頭文件(.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中的原話:
- 當編譯器遇到一個模板定義時,它並不生成代碼。只有當我們實例化出模板的一個特定版本時,編譯器纔會生成代碼。當我們使用(而不是定義)模板時,編譯器才生成代碼。
- 通常當我們調用一個函數時,編譯器只需要掌握函數的聲明,類似的,當我們使用一個類類型的對象時,類定義必須是可用的,但是成員函數的定義不必已經出現。因此可以將類定義和函數聲明放在頭文件中,而將普通函數和類的成員函數的定義放在源文件中。
- 模板則不同:爲了生成一個實例化版本,編譯器需要掌握函數模板或類模板成員函數的定義。因此,與非模板代碼不同,模板的頭文件通常既包括聲明也包括定義。
我的理解是:
- 模板類不是一個具體的類型,沒有相關的代碼,只有在對模板類進行實例化後,纔會生成一個具體的類和類內部的成員函數。所以如果沒有實例化,是不可能使用模板類中的成員函數的。
- 而如果不對模板類進行顯式實例化的話,那麼只有在傳入模板類型實參給模板類,並使用這個模板類之後,編譯器纔會給這個已經指定類型實參的模板類生成具體的代碼。
舉個例子,雖然stack<int>
和stack<char>
使用的是同一個模板類,但是它們是兩個不同的類。因此這兩個具體類內部的成員(成員變量和成員函數)也是不同的。編譯器會在使用stack<int>
和stack<char>
的地方生成相應的類成員信息。
從執行流程來看,在main.cpp
中通過TreeNode<int>
來調用模板類的成員函數時,實際上調用的是TreeNode<int>
這一個具體類的成員函數,而main.cpp
和TreeNode.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++的模板生成實例化相關。我覺得自己還是對模板的生成、實例化等內容不熟悉,不然這個問題還是很容易解決的。