C++模板背后的黑箱操作:編譯器
一、編譯器如何處理模板
1.模板代碼的處理
為了理解模板的復(fù)雜性,你需要了解編譯器是如何處理模板代碼的。當(dāng)編譯器遇到模板方法定義時(shí),它會(huì)進(jìn)行語(yǔ)法檢查,但實(shí)際上不會(huì)編譯模板。編譯器不能編譯模板定義,因?yàn)樗恢肋@些模板將用于哪些類(lèi)型。編譯器不可能為像 x = y 這樣的代碼生成代碼,而不知道 x 和 y 的類(lèi)型。
當(dāng)編譯器遇到模板的實(shí)例化,例如 Grid<int>,它會(huì)通過(guò)將類(lèi)模板定義中的每個(gè) T 替換為 int 來(lái)為 int 版本的 Grid 模板編寫(xiě)代碼。當(dāng)編譯器遇到模板的不同實(shí)例化,例如 Grid<SpreadsheetCell>,它會(huì)為 SpreadsheetCell 編寫(xiě)另一個(gè)版本的 Grid 類(lèi)。編譯器只是寫(xiě)出了如果沒(méi)有模板支持,你需要為每種元素類(lèi)型編寫(xiě)單獨(dú)類(lèi)時(shí)的代碼。這里沒(méi)有魔法;模板只是自動(dòng)化了一個(gè)煩人的過(guò)程。如果你在程序中沒(méi)有為任何類(lèi)型實(shí)例化類(lèi)模板,那么類(lèi)方法定義就永遠(yuǎn)不會(huì)被編譯。
這種實(shí)例化過(guò)程解釋了為什么你需要在定義的各個(gè)地方使用 Grid<T> 語(yǔ)法。當(dāng)編譯器為特定類(lèi)型(如 int)實(shí)例化模板時(shí),它會(huì)將 T 替換為 int,使 Grid<int> 成為該類(lèi)型。
2.選擇性實(shí)例化
對(duì)于隱式類(lèi)模板實(shí)例化,如以下示例:
Grid<int> myIntGrid;
編譯器總是為類(lèi)模板的所有虛擬方法生成代碼。然而,對(duì)于非虛擬方法,編譯器只為你實(shí)際調(diào)用的那些非虛擬方法生成代碼。例如,給定前面的 Grid 類(lèi)模板,假設(shè)你在 main() 中寫(xiě)了這樣的代碼(僅此代碼):
Grid<int> myIntGrid;
myIntGrid.at(0, 0) = 10;
編譯器僅為 int 版本的 Grid 生成無(wú)參數(shù)構(gòu)造函數(shù)、析構(gòu)函數(shù)和非 const 的 at() 方法。它不會(huì)生成其他方法,如拷貝構(gòu)造函數(shù)、賦值運(yùn)算符或 getHeight()。這被稱(chēng)為選擇性實(shí)例化。
存在的風(fēng)險(xiǎn)是,某些類(lèi)模板方法中的編譯錯(cuò)誤可能會(huì)被忽略。未使用的類(lèi)模板方法可能包含語(yǔ)法錯(cuò)誤,因?yàn)檫@些不會(huì)被編譯。這使得測(cè)試所有代碼的語(yǔ)法錯(cuò)誤變得困難。
你可以通過(guò)使用顯式模板實(shí)例化來(lái)強(qiáng)制編譯器為所有方法(虛擬和非虛擬)生成代碼。以下是一個(gè)示例:
template class Grid<int>;
注意:顯式模板實(shí)例化有助于發(fā)現(xiàn)錯(cuò)誤,因?yàn)樗鼜?qiáng)制編譯器編譯所有即使未使用的類(lèi)模板方法。使用顯式模板實(shí)例化時(shí),不要只嘗試使用基本類(lèi)型(如 int)實(shí)例化類(lèi)模板,還要嘗試使用更復(fù)雜的類(lèi)型(如 string)。
二、模板對(duì)類(lèi)型的要求
1.類(lèi)型獨(dú)立的代碼編寫(xiě)
當(dāng)你編寫(xiě)與類(lèi)型無(wú)關(guān)的代碼時(shí),必須對(duì)這些類(lèi)型做出某些假設(shè)。例如,在 Grid 類(lèi)模板中,你假設(shè)元素類(lèi)型(由 T 表示)是可銷(xiāo)毀的、可拷貝/移動(dòng)構(gòu)造的,以及可拷貝/移動(dòng)賦值的。
當(dāng)編譯器嘗試用不支持類(lèi)模板方法所使用的所有操作的類(lèi)型來(lái)實(shí)例化模板時(shí),代碼將無(wú)法編譯,且錯(cuò)誤消息通常相當(dāng)晦澀難懂。
然而,即使你想使用的類(lèi)型不支持類(lèi)模板的所有方法所需的操作,你也可以利用選擇性實(shí)例化來(lái)使用某些方法而不是其他方法。
2.C++20 引入的概念(Concepts)
C++20 引入了概念(concepts),允許你為模板參數(shù)編寫(xiě)編譯器可以解釋和驗(yàn)證的要求。如果傳遞給模板實(shí)例化的模板參數(shù)不滿(mǎn)足這些要求,編譯器可以生成更易讀的錯(cuò)誤消息。后面將討論概念。
概念為模板編程增加了額外的類(lèi)型安全性,它通過(guò)為模板參數(shù)提供一個(gè)明確的接口合約來(lái)實(shí)現(xiàn)。這種方式不僅可以防止類(lèi)型不匹配的問(wèn)題,還可以改善模板錯(cuò)誤消息的可讀性,從而使模板代碼更容易維護(hù)和理解。
三、類(lèi)模板代碼的文件
在類(lèi)模板中,類(lèi)模板定義和方法定義必須對(duì)任何使用它們的源文件可用。有幾種機(jī)制可以實(shí)現(xiàn)這一點(diǎn):
1.方法定義與類(lèi)模板定義在同一文件
你可以將方法定義直接放在定義類(lèi)模板本身的模塊接口文件中。當(dāng)你在另一個(gè)源文件中導(dǎo)入這個(gè)模塊以使用模板時(shí),編譯器將能夠訪問(wèn)它所需的所有代碼。這種機(jī)制用于之前的 Grid 實(shí)現(xiàn)。
2.方法定義在單獨(dú)的文件
或者,你可以將類(lèi)模板方法定義放在一個(gè)單獨(dú)的模塊接口分區(qū)文件中。然后,你還需要將類(lèi)模板定義放在自己的分區(qū)中。例如,Grid 類(lèi)模板的主模塊接口文件可能如下所示:
export module grid;
export import :definition;
export import :implementation;
這導(dǎo)入并導(dǎo)出了兩個(gè)模塊分區(qū):定義(definition)和實(shí)現(xiàn)(implementation)。類(lèi)模板定義在定義分區(qū)中定義:
export module grid:definition;
import <vector>;
import <optional>;
export template <typename T> class Grid { ... };
方法的實(shí)現(xiàn)位于實(shí)現(xiàn)分區(qū)中,該分區(qū)還需要導(dǎo)入定義分區(qū),因?yàn)樗枰?nbsp;Grid 類(lèi)模板定義:
export module grid:implementation;
import :definition;
import <vector>;
...
export template <typename T> Grid<T>::Grid(size_t width, size_t height)
: m_width { width }, m_height { height } { ... }