玩轉C++方法模板,編程技能秒提升
方法模板
C++ 中的方法模板
C++ 允許對類的單個方法進行模板化。這種方法被稱為方法模板,可以存在于普通類或類模板中。編寫方法模板實際上就是為許多不同類型編寫該方法的不同版本。方法模板對于類模板中的賦值運算符和拷貝構造函數非常有用。
警告:虛方法和析構函數不能是方法模板。
考慮僅有一個模板參數的原始 Grid 模板:元素類型。您可以實例化許多不同類型的網格,例如 int 和 double:
Grid<int> myIntGrid;
Grid<double> myDoubleGrid;
然而,Grid<int> 和 Grid<double> 是兩種不同的類型。如果你編寫一個接受 Grid<double> 類型對象的函數,你不能傳遞 Grid<int>。即使你知道 int 網格的元素可以復制到 double 網格的元素中,因為 int 可以轉換為 double,但你不能將 Grid<int> 類型的對象賦值給 Grid<double> 類型的對象,或從 Grid<int> 構造 Grid<double>。以下兩行都無法編譯:
myDoubleGrid = myIntGrid; // 無法編譯
Grid<double> newDoubleGrid { myIntGrid }; // 無法編譯
問題在于 Grid 模板的拷貝構造函數和賦值運算符定義如下:
Grid(const Grid& src);
Grid& operator=(const Grid& rhs);
等效于:
Grid(const Grid<T>& src);
Grid<T>& operator=(const Grid<T>& rhs);
Grid 的拷貝構造函數和 operator= 都需要一個 const Grid<T>& 的引用。當你實例化 Grid<double> 并嘗試調用拷貝構造函數和 operator= 時,編譯器生成以下原型的方法:
Grid(const Grid<double>& src);
Grid<double>& operator=(const Grid<double>& rhs);
注意,生成的 Grid<double> 類中沒有接受 Grid<int> 的構造函數或 operator=。
幸運的是,您可以通過向 Grid 類添加模板化的拷貝構造函數和賦值運算符的版本來糾正這種疏忽,從而生成將一個網格類型轉換為另一個網格類型的方法。以下是新的 Grid 類模板定義:
export template <typename T>
class Grid {
public:
template <typename E>
Grid(const Grid<E>& src);
template <typename E>
Grid& operator=(const Grid<E>& rhs);
void swap(Grid& other) noexcept;
// 為了簡潔省略部分內容
};
原始的拷貝構造函數和拷貝賦值運算符不能被移除。如果 E 等于 T,編譯器不會調用這些新的模板化拷貝構造函數和模板化拷貝賦值運算符。首先查看新的模板化拷貝構造函數:
template <typename E>
Grid(const Grid<E>& src);
您可以看到有另一個模板聲明,使用不同的類型名 E(代表“元素”)。類在一個類型 T 上進行模板化,新的拷貝構造函數也在不同的類型 E 上進行模板化。這種雙重模板化允許您將一個類型的網格復制到另一個類型。以下是新拷貝構造函數的定義:
template <typename T>
template <typename E>
Grid<T>::Grid(const Grid<E>& src)
: Grid { src.getWidth(), src.getHeight() } {
// 此構造函數的 ctor-initializer 首先委托給非拷貝構造函數來分配適當的內存量。
// 下一步是復制數據。
for (size_t i { 0 }; i < m_width; i++) {
for (size_t j { 0 }; j < m_height; j++) {
m_cells[i][j] = src.at(i, j);
}
}
}
如您所見,您必須在成員模板行(帶 E 參數)之前聲明類模板行(帶 T 參數)。您不能像這樣組合它們:
template <typename T, typename E> // 對于嵌套模板構造函數錯誤!
Grid<T>::Grid(const Grid<E>& src)
除了構造函數定義之前的額外模板參數行外,注意您必須使用公共訪問方法 getWidth()、getHeight() 和 at() 來訪問 src 的元素。那是因為您正在復制到的對象是 Grid<T> 類型的,而您正在復制的對象是 Grid<E> 類型的。它們不是同一類型,所以您必須使用公共方法。
swap() 方法實現如下:
template <typename T>
void Grid<T>::swap(Grid& other) noexcept {
std::swap(m_width, other.m_width);
std::swap(m_height, other.m_height);
std::swap(m_cells, other.m_cells);
}
模板化賦值運算符接受一個 const Grid<E>&,但返回一個 Grid<T>&:
template <typename T>
template <typename E>
Grid<T>& Grid<T>::operator=(const Grid<E>& rhs) {
// 使用復制-交換習慣用法
Grid<T> temp { rhs }; // 在臨時實例中完成所有工作。
swap(temp); // 僅通過非拋出操作提交工作。
return *this;
}
這個賦值運算符的實現使用了復制-交換習慣用法。swap() 方法只能交換同一類型的 Grids,但這沒關系,因為這個模板化賦值運算符首先使用模板化拷貝構造函數將給定的 Grid<E> 轉換為 Grid<T>,名為 temp。之后,它使用 swap() 方法將這個臨時的 Grid<T> 與 this(也是 Grid<T> 類型)交換。
使用非類型參數的方法模板
不同大小網格的賦值和拷貝
在先前的例子中,使用整數模板參數 HEIGHT 和 WIDTH,主要問題是高度和寬度成為了類型的一部分。這種限制阻止了將一個尺寸的網格賦值給另一個不同尺寸的網格。然而,在某些情況下,將一個大小的網格賦值或拷貝給不同大小的網格是可取的。與其使目標對象成為源對象的完美克隆,不如只從源數組中復制適合目標數組的元素,并在源數組較小的維度上用默認值填充目標數組。使用賦值運算符和拷貝構造函數的方法模板,您可以做到這一點,從而允許賦值和拷貝不同大小的網格。以下是類定義:
export template <typename T, size_t WIDTH = 10, size_t HEIGHT = 10>
class Grid {
public:
Grid() = default;
virtual ~Grid() = default;
// 明確默認拷貝構造函數和賦值運算符。
Grid(const Grid& src) = default;
Grid& operator=(const Grid& rhs) = default;
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid(const Grid<E, WIDTH2, HEIGHT2>& src);
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid& operator=(const Grid<E, WIDTH2, HEIGHT2>& rhs);
void swap(Grid& other) noexcept;
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];
};
這個新定義包括拷貝構造函數和賦值運算符的方法模板,以及一個輔助方法 swap()。注意,非模板化的拷貝構造函數和賦值運算符是明確默認的(因為用戶聲明了析構函數)。它們僅復制或賦值源對象的 m_cells 到目標對象,這正是對于相同大小的兩個網格所希望的語義。
下面是模板化拷貝構造函數的實現:
template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid<T, WIDTH, HEIGHT>::Grid(const Grid<E, WIDTH2, HEIGHT2>& src) {
for (size_t i { 0 }; i < WIDTH; i++) {
for (size_t j { 0 }; j < HEIGHT; j++) {
if (i < WIDTH2 && j < HEIGHT2) {
m_cells[i][j] = src.at(i, j);
} else {
m_cells[i][j].reset();
}
}
}
}
請注意,此拷貝構造函數僅從 src 中復制 x 和 y 維度上的 WIDTH 和 HEIGHT 元素,即使 src 比這更大。如果 src 在任一維度上較小,則額外位置的 std::optional 對象使用 reset() 方法重置。
下面是 swap() 方法和賦值運算符 operator= 的實現:
template <typename T, size_t WIDTH, size_t HEIGHT>
void Grid<T, WIDTH, HEIGHT>::swap(Grid& other) noexcept {
std::swap(m_cells, other.m_cells);
}
template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid<T,
WIDTH, HEIGHT>& Grid<T, WIDTH, HEIGHT>::operator=(
const Grid<E, WIDTH2, HEIGHT2>& rhs) {
// 使用復制-交換習慣用法
Grid<T, WIDTH, HEIGHT> temp { rhs }; // 在臨時實例中完成所有工作。
swap(temp); // 僅通過非拋出操作提交工作。
return *this;
}
這個賦值運算符的實現使用了復制-交換習慣用法。swap() 方法只能交換相同類型的 Grids,但這是可以的,因為這個模板化賦值運算符首先使用模板化拷貝構造函數將給定的 Grid<E, WIDTH2, HEIGHT2> 轉換為 Grid<T, WIDTH, HEIGHT>,稱為 temp。之后,它使用 swap() 方法交換這個臨時 Grid<T, WIDTH, HEIGHT> 和 this。