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