掌握C++模板的藝術:類型參數、默認值和自動推導
一、模板參數
1.類型模板參數
在 Grid 示例中,Grid 模板有一個模板參數:存儲在網格中的類型。編寫類模板時,您需要在尖括號內指定參數列表,例如:
template <typename T>
這個參數列表類似于函數或方法中的參數列表。與函數和方法一樣,你可以編寫具有任意多個模板參數的類。此外,這些參數不必是類型,它們可以有默認值。
2.非類型模板參數
非類型參數是普通參數,如整數和指針——這類參數你可能已經在函數和方法中很熟悉了。然而,非類型模板參數只能是整型(char、int、long 等)、枚舉類型、指針、引用、std::nullptr_t、auto、auto& 和 auto*。C++20 還允許浮點類型和類類型的非類型模板參數。后者有很多限制,在本文中不再詳細討論。
在 Grid 類模板中,你可以使用非類型模板參數來指定網格的高度和寬度,而不是在構造函數中指定。在模板列表中指定非類型參數而不是在構造函數中指定的主要優點是這些值在代碼編譯之前就已知。回想一下,編譯器通過在編譯之前替換模板參數來生成模板實例的代碼。因此,你可以在實現中使用普通的二維數組,而不是動態調整大小的向量數組。以下是帶有更改的新類定義:
export template <typename T, size_t WIDTH, size_t HEIGHT>
class Grid {
public:
Grid() = default;
virtual ~Grid() = default;
// 明確默認復制構造函數和賦值運算符。
Grid(const Grid& src) = default;
Grid& operator=(const Grid& rhs) = default;
std::optional<T>& at(size_t x, size_t y);
const std::optional<T>& at(size_t x, size_t y) const;
size_t getHeight() const { return HEIGHT; }
size_t getWidth() const { return WIDTH; }
private:
void verifyCoordinate(size_t x, size_t y) const;
std::optional<T> m_cells[WIDTH][HEIGHT];
};
注意,模板參數列表需要三個參數:存儲在網格中的對象類型,以及網格的寬度和高度。寬度和高度用于創建存儲對象的二維數組。下面是類方法的定義:
// 類方法定義
template <typename T, size_t WIDTH, size_t HEIGHT>
void Grid<T, WIDTH, HEIGHT>::verifyCoordinate(size_t x, size_t y) const {
if (x >= WIDTH) {
throw std::out_of_range { std::format("{} must be less than {}.", x, WIDTH) };
}
if (y >= HEIGHT) {
throw std::out_of_range { std::format("{} must be less than {}.", y, HEIGHT) };
}
}
template <typename T, size_t WIDTH, size_t HEIGHT>
const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y) const {
verifyCoordinate(x, y);
return m_cells[x][y];
}
template <typename T, size_t WIDTH, size_t HEIGHT>
std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y) {
return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));
}
注意,之前你在哪里指定了 Grid<T>,現在你必須指定 Grid<T, WIDTH, HEIGHT> 來指定三個模板參數。你可以這樣實例化并使用這個模板:
Grid<int,
10, 10> myGrid;
Grid<int, 10, 10> anotherGrid;
myGrid.at(2, 3) = 42;
anotherGrid = myGrid;
cout << anotherGrid.at(2, 3).value_or(0);
這段代碼看起來很棒,但不幸的是,存在比你最初預期的更多限制。首先,你不能使用非常量整數來指定高度或寬度。以下代碼無法編譯:
size_t height { 10 };
Grid<int, 10, height> testGrid; // 無法編譯
然而,如果你將高度定義為常量,則可以編譯:
const size_t height { 10 };
Grid<int, 10, height> testGrid; // 可編譯并工作
具有正確返回類型的 constexpr 函數也可以工作。例如,如果你有一個返回 size_t 的 constexpr 函數,你可以用它來初始化高度模板參數:
constexpr size_t getHeight() { return 10; }
...
Grid<double, 2, getHeight()> myDoubleGrid;
第二個限制可能更重要。現在寬度和高度是模板參數,它們是每個網格類型的一部分。這意味著 Grid<int,10,10> 和 Grid<int,10,11> 是兩種不同的類型。你不能將一種類型的對象賦值給另一種類型的對象,也不能將一種類型的變量傳遞給期望另一種類型變量的函數或方法。
注意:非類型模板參數成為實例化對象類型規范的一部分。
二、類模板參數的默認值
設置高度和寬度的默認值
如果您繼續使用高度和寬度作為模板參數的方法,您可能想為 Grid<T> 類構造函數中之前的高度和寬度非類型模板參數提供默認值。C++ 允許您使用類似的語法為模板參數提供默認值。同時,您也可以為 T 類型參數提供默認值。下面是類定義:
export template <typename T = int, size_t WIDTH = 10, size_t HEIGHT = 10>
class Grid {
// 其余部分與之前版本相同
};
在方法定義的模板規范中,您不需要為 T、WIDTH 和 HEIGHT 指定默認值。例如,這是 at() 方法的實現:
template <typename T, size_t WIDTH, size_t HEIGHT>
const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(size_t x, size_t y) const {
verifyCoordinate(x, y);
return m_cells[x][y];
}
現在,您可以在沒有任何模板參數的情況下實例化 Grid,只需指定元素類型,元素類型和寬度,或元素類型、寬度和高度:
Grid<> myIntGrid;
Grid<int> myGrid;
Grid<int, 5> anotherGrid;
Grid<int, 5, 5> aFourthGrid;
請注意,如果您不指定任何類模板參數,您仍然需要指定一組空的尖括號。例如,以下代碼無法編譯!
Grid myIntGrid;
類模板參數列表中默認參數的規則與函數或方法相同;也就是說,您可以從右邊開始為參數提供默認值。
三、類模板參數推導(CTAD)
1.自動推導模板參數
類模板參數推導允許編譯器自動從傳遞給類模板構造函數的參數推導出模板參數。例如,標準庫中有一個名為 std::pair 的類模板,在 <utility> 中定義,并在第1章中介紹。pair 存儲兩個可能不同類型的值,通常需要指定為模板參數。例如:
pair<int, double> pair1 { 1, 2.3 };
為了避免編寫模板參數,可以使用一個名為 std::make_pair() 的輔助函數模板。編寫自己的函數模板的細節將在本章后面討論。函數模板一直支持基于傳遞給函數模板的參數自動推導模板參數。因此,make_pair() 能夠根據傳遞給它的值自動推導出模板類型參數。例如,編譯器為以下調用推導出 pair<int, double>:
auto pair2 { make_pair(1, 2.3) };
使用類模板參數推導(CTAD),不再需要這樣的輔助函數模板。編譯器現在會根據傳遞給構造函數的參數自動推導出模板類型參數。對于 pair 類模板,您可以簡單地編寫以下代碼:
pair pair3 { 1, 2.3 }; // pair3 的類型為 pair<int, double>
當然,這僅在類模板的所有模板參數要么具有默認值,要么用作構造函數中的參數,從而可以推導出來時才有效。請注意,CTAD 要求有一個初始化器才能工作。以下是非法的:
pair pair4;
許多標準庫類支持 CTAD,例如 vector、array 等。
注意:這種類型推導對 std::unique_ptr 和 shared_ptr 無效。您向它們的構造函數傳遞 T*,這意味著編譯器必須在推導 <T> 或 <T[]> 之間選擇,如果選錯了就會很危險。因此,請記住,對于 unique_ptr 和 shared_ptr,您需要繼續使用 make_unique() 和 make_shared()。
2.用戶定義的推導指南
您也可以編寫自己的用戶定義推導指南來幫助編譯器。這些指南允許您編寫模板參數如何被推導的規則。這是一個高級主題,所以不會詳細討論,但會給出一個示例來展示它們的強大功能。假設您有以下 SpreadsheetCell 類模板:
template <typename T>
class SpreadsheetCell {
public:
SpreadsheetCell(T t) : m_content { move(t) } { }
const T& getContent() const { return m_content; }
private:
T m_content;
};
使用自動模板參數推導,您可以創建一個 std::string 類型的 SpreadsheetCell:
string myString { "Hello World!" };
SpreadsheetCell cell { myString };
然而,如果您將 const char 傳遞給 SpreadsheetCell 構造函數,則類型 T 被推導為 const char,這不是您想要的!您可以創建以下用戶定義的推導指南,當向構造函數傳遞 const char* 作為參數時,使其將 T 推導為 std::string:
SpreadsheetCell(const char*) -> SpreadsheetCell<std::string>;
這個指南必須在類定義之外但在與 SpreadsheetCell 類相同的命名空間內定義。通用語法如下。explicit 關鍵字是可選的,其行為與構造函數的 explicit 相同。通常,這樣的推導指南也是模板。
explicit TemplateName(Parameters) -> DeducedTemplate;