深入理解C++20:類與對象的高級特性及運(yùn)算符重載
類與對象的高級特性
1.常量靜態(tài)數(shù)據(jù)成員
在你的類中,可以聲明 const 數(shù)據(jù)成員,這意味著它們在創(chuàng)建和初始化后不能被改變。當(dāng)常量僅適用于類時(shí),應(yīng)該使用 static const(或 const static)數(shù)據(jù)成員來代替全局常量,這也稱為類常量。整型和枚舉類型的 static const 數(shù)據(jù)成員即使不將它們作為內(nèi)聯(lián)變量,也可以在類定義內(nèi)部定義和初始化。例如,你可能想要為Spreadsheet指定一個(gè)最大高度和寬度。如果用戶嘗試構(gòu)造一個(gè)高度或?qū)挾瘸^最大值的Spreadsheet,將使用最大值代替。你可以將最大高度和寬度作為 Spreadsheet 類的 static const 成員:
export class Spreadsheet {
public:
// 省略簡略性
static const size_t MaxHeight { 100 };
static const size_t MaxWidth { 100 };
};
你可以在構(gòu)造函數(shù)中使用這些新常量,如下所示:
Spreadsheet::Spreadsheet(size_t width, size_t height)
: m_id { ms_counter++ },
m_width { min(width, MaxWidth) } // std::min() 需要 <algorithm>
m_height { min(height, MaxHeight) }
{
// 省略簡略性
}
注意,你也可以選擇在寬度或高度超過最大值時(shí)拋出異常,而不是自動(dòng)將寬度和高度限制在其最大值內(nèi)。但是,當(dāng)你從構(gòu)造函數(shù)中拋出異常時(shí),析構(gòu)函數(shù)將不會(huì)被調(diào)用,所以你需要小心處理這一點(diǎn)。這在第14章詳細(xì)討論了錯(cuò)誤處理。
2.數(shù)據(jù)成員的不同種類
此類常量也可以用作參數(shù)的默認(rèn)值。記住,你只能為從最右邊參數(shù)開始的一連串參數(shù)提供默認(rèn)值。這里有一個(gè)例子:
export class Spreadsheet {
public:
Spreadsheet(size_t width = MaxWidth, size_t height = MaxHeight);
// 省略簡略性
};
3.引用數(shù)據(jù)成員
Spreadsheet和 SpreadsheetCells 很棒,但它們本身并不構(gòu)成一個(gè)有用的應(yīng)用程序。你需要代碼來控制整個(gè)Spreadsheet程序,你可以將其打包到一個(gè)名為 SpreadsheetApplication 的類中。假設(shè)我們希望每個(gè) Spreadsheet 都存儲(chǔ)對應(yīng)用程序?qū)ο蟮囊谩preadsheetApplication 類的確切定義此刻并不重要,因此下面的代碼簡單地將其定義為一個(gè)空類。Spreadsheet 類被修改為包含一個(gè)新的引用數(shù)據(jù)成員,稱為 m_theApp:
export class SpreadsheetApplication {
};
export class Spreadsheet {
public:
Spreadsheet(size_t width, size_t height, SpreadsheetApplication& theApp);
// 省略簡略性
private:
// 省略簡略性
SpreadsheetApplication& m_theApp;
};
這個(gè)定義為數(shù)據(jù)成員添加了一個(gè) SpreadsheetApplication 引用。建議在這種情況下使用引用而不是指針,因?yàn)?nbsp;Spreadsheet 應(yīng)該總是引用一個(gè) SpreadsheetApplication,而指針則不能保證這一點(diǎn)。請注意,將應(yīng)用程序的引用存儲(chǔ)起來僅是為了演示引用作為數(shù)據(jù)成員的用法。不建議以這種方式將 Spreadsheet 和 SpreadsheetApplication 類耦合在一起,而是使用模型-視圖-控制器(MVC)范例。
在其構(gòu)造函數(shù)中,應(yīng)用程序引用被賦給每個(gè) Spreadsheet。引用不能存在而不指向某些東西,因此 m_theApp 必須在構(gòu)造函數(shù)的 ctor-initializer 中被賦值:
Spreadsheet::Spreadsheet(size_t width, size_t height, SpreadsheetApplication& theApp)
: m_id { ms_counter++ },
m_width { std::min(width, MaxWidth) },
m_height { std::min(height, MaxHeight) },
m_theApp { theApp }
{
// 省略簡略性
}
你還必須在拷貝構(gòu)造函數(shù)中初始化引用成員。這是自動(dòng)處理的,因?yàn)?nbsp;Spreadsheet 拷貝構(gòu)造函數(shù)委托給非拷貝構(gòu)造函數(shù),后者初始化了引用數(shù)據(jù)成員。記住,一旦你初始化了一個(gè)引用,你就不能改變它所引用的對象。在賦值操作符中不可能對引用進(jìn)行賦值。根據(jù)你的用例,這可能意味著你的類不能為含有引用數(shù)據(jù)成員的類提供賦值操作符。如果是這種情況,賦值操作符通常被標(biāo)記為刪除。
最后,引用數(shù)據(jù)成員也可以標(biāo)記為 const。例如,你可能決定 Spreadsheets 只應(yīng)該對應(yīng)用程序?qū)ο笥幸粋€(gè)常量引用。你可以簡單地更改類定義,將 m_theApp 聲明為對常量的引用:
export class Spreadsheet {
public:
Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
// 省略簡略性
private:
// 省略簡略性
const SpreadsheetApplication& m_theApp;
};
3.嵌套類
類定義不僅可以包含成員函數(shù)和數(shù)據(jù)成員,還可以編寫嵌套類和結(jié)構(gòu)體,聲明類型別名或創(chuàng)建枚舉類型。在類內(nèi)部聲明的任何內(nèi)容都在該類的作用域內(nèi)。如果它是公開的,你可以通過使用類名加上作用域解析運(yùn)算符(ClassName::)來在類外部訪問它。
例如,你可能會(huì)決定 SpreadsheetCell 類實(shí)際上是 Spreadsheet 類的一部分。由于它成為 Spreadsheet 類的一部分,你可能會(huì)將其重命名為 Cell。你可以像這樣定義它們:
export class Spreadsheet {
public:
class Cell {
public:
Cell() = default;
Cell(double initialValue);
// 省略簡略性
};
Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
// 省略其他 Spreadsheet 聲明
};
現(xiàn)在,Cell 類在 Spreadsheet 類內(nèi)部定義,所以在 Spreadsheet 類外部引用 Cell 時(shí),你必須使用 Spreadsheet:: 作用域來限定名稱。這甚至適用于方法定義。例如,Cell 的雙精度構(gòu)造函數(shù)現(xiàn)在看起來像這樣:
Spreadsheet::Cell::Cell(double initialValue)
: m_value { initialValue } {
}
即使是在 Spreadsheet 類本身的方法的返回類型(但不是參數(shù))中,也必須使用此語法:
Spreadsheet::Cell& Spreadsheet::getCellAt(size_t x, size_t y) {
verifyCoordinate(x, y);
return m_cells[x][y];
}
直接在 Spreadsheet 類內(nèi)部完全定義嵌套的 Cell 類會(huì)使 Spreadsheet 類的定義變得臃腫。你可以通過僅在 Spreadsheet 類中包含 Cell 的前向聲明,然后分別定義 Cell 類來緩解這種情況:
export class Spreadsheet {
public:
class Cell;
Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
// 省略其他 Spreadsheet 聲明
};
class Spreadsheet::Cell {
public:
Cell() = default;
Cell(double initialValue);
// 省略簡略性
};
普通的訪問控制適用于嵌套類定義。如果你聲明了一個(gè)私有或受保護(hù)的嵌套類,你只能從外部類內(nèi)部使用它。嵌套類可以訪問外部類的所有受保護(hù)和私有成員。而外部類只能訪問嵌套類的公共成員。
4.類內(nèi)部的枚舉類型
枚舉類型也可以是類的數(shù)據(jù)成員。例如,你可以添加對 SpreadsheetCell 類的單元格著色支持,如下所示:
export class SpreadsheetCell {
public:
// 省略簡略性
enum class Color {
Red = 1, Green, Blue, Yellow
};
void setColor(Color color);
Color getColor() const;
private:
// 省略簡略性
Color m_color { Color::Red };
};
setColor() 和 getColor() 方法的實(shí)現(xiàn)很直接:
void SpreadsheetCell::setColor(Color color) {
m_color = color;
}
SpreadsheetCell::Color SpreadsheetCell::getColor() const {
return m_color;
}
新方法的使用方式如下:
SpreadsheetCell myCell { 5 };
myCell.setColor(SpreadsheetCell::Color::Blue);
auto color { myCell.getColor() };
運(yùn)算符重載
你經(jīng)常需要對對象執(zhí)行操作,例如添加它們、比較它們,或?qū)⑺鼈兞魅肓鞒鑫募@纾琒preadsheet只有在你可以對其執(zhí)行算術(shù)操作時(shí)才有用,比如求一整行單元格的和。
1.重載比較運(yùn)算符
在你的類中定義比較運(yùn)算符,如>、<、<=、>=、==和!=,是非常有用的。C++20標(biāo)準(zhǔn)為這些運(yùn)算符帶來了很多變化,并增加了三元比較運(yùn)算符,即太空船運(yùn)算符<=>,在第1章中有介紹。為了更好地理解C++20所提供的內(nèi)容,讓我們先來看看在C++20之前你需要做些什么,以及在你的編譯器還不支持三元比較運(yùn)算符時(shí)你仍需要做些什么。
就像基本的算術(shù)運(yùn)算符一樣,C++20之前的六個(gè)比較運(yùn)算符應(yīng)該是全局函數(shù),這樣你可以在運(yùn)算符的左右兩邊的參數(shù)上使用隱式轉(zhuǎn)換。比較運(yùn)算符都返回一個(gè)布爾值。當(dāng)然,你可以更改返回類型,但這并不推薦。這里是聲明,你需要用==、<、>、!=、<=和>=替換<op>,從而產(chǎn)生六個(gè)函數(shù):
export class SpreadsheetCell { /* 省略以便簡潔 */ };
export bool operator<op>(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
以下是operator==的定義。其他的定義類似。
bool operator==(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs) {
return (lhs.getValue() == rhs.getValue());
}
注意:前述重載的比較運(yùn)算符正在比較雙精度值。大多數(shù)時(shí)候,對浮點(diǎn)值進(jìn)行等于或不等于測試并不是一個(gè)好主意。你應(yīng)該使用所謂的epsilon測試,但這超出了本書的范圍。在具有更多數(shù)據(jù)成員的類中,比較每個(gè)數(shù)據(jù)成員可能很痛苦。然而,一旦你實(shí)現(xiàn)了==和<,你就可以用這兩個(gè)運(yùn)算符來寫其它的比較運(yùn)算符。例如,這里是一個(gè)使用operator<的operator>=定義:
bool operator>=(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs) {
return !(lhs < rhs);
}
你可以使用這些運(yùn)算符來比較SpreadsheetCells與其他SpreadsheetCells,也可以與雙精度和整型比較:
if (myCell > aThirdCell || myCell < 10) {
cout << myCell.getValue() << endl;
}
正如你所見,你需要編寫六個(gè)不同的函數(shù)來支持六個(gè)比較運(yùn)算符,這只是為了比較兩個(gè)SpreadsheetCells。隨著當(dāng)前六個(gè)實(shí)現(xiàn)的比較函數(shù),可以將SpreadsheetCell與一個(gè)雙精度值進(jìn)行比較,因?yàn)殡p精度參數(shù)被隱式轉(zhuǎn)換為SpreadsheetCell。如前所述,這種隱式轉(zhuǎn)換可能效率低下,因?yàn)樾枰獎(jiǎng)?chuàng)建臨時(shí)對象。就像之前的operator+一樣,你可以通過實(shí)現(xiàn)顯式函數(shù)來避免與雙精度的比較。對于每個(gè)運(yùn)算符<op>,你將需要以下三個(gè)重載:
bool operator<op>(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);
bool operator<op>(double lhs, const SpreadsheetCell& rhs);
bool operator<op>(const SpreadsheetCell& lhs, double rhs);
如果你想支持所有比較運(yùn)算符,那么需要編寫很多重復(fù)的代碼!
2.C++20
現(xiàn)在讓我們轉(zhuǎn)換一下思路,看看C++20帶來了什么。C++20極大地簡化了為你的類添加比較運(yùn)算符的支持。首先,使用C++20,實(shí)際上建議將operator==實(shí)現(xiàn)為類的成員函數(shù),而不是全局函數(shù)。還要注意,添加[[nodiscard]]屬性是個(gè)好主意,這樣運(yùn)算符的結(jié)果就不能被忽略了。這里是一個(gè)例子:
[[nodiscard]] bool operator==(const SpreadsheetCell& rhs) const;
使用C++20,這一個(gè)operator==重載就可以使以下比較工作:
if (myCell == 10) {
cout << "myCell == 10\n";
}
if (10 == myCell) {
cout << "10 == myCell\n";
}
例如10==myCell這樣的表達(dá)式會(huì)被C++20編譯器重寫為myCell==10,可以調(diào)用operator==成員函數(shù)。此外,通過實(shí)現(xiàn)operator==,C++20會(huì)自動(dòng)增加對!=的支持。
接下來,為了實(shí)現(xiàn)對完整套比較運(yùn)算符的支持,在C++20中你只需要實(shí)現(xiàn)一個(gè)額外的重載運(yùn)算符,即operator<=>。一旦你的類有了operator==和<=>的重載,C++20會(huì)自動(dòng)為所有六個(gè)比較運(yùn)算符提供支持!對于SpreadsheetCell類,operator<=>如下所示:
[[nodiscard]] std::partial_ordering operator<=>(const SpreadsheetCell& rhs) const;
注意:C++20編譯器不會(huì)用<=>重寫==或!=比較,這是為了避免性能問題,因?yàn)轱@式實(shí)現(xiàn)operator==通常比使用<=>更高效。
SpreadsheetCell中存儲(chǔ)的值是一個(gè)雙精度值。請記住,從第1章開始,浮點(diǎn)類型只有部分排序,這就是為什么重載返回std::partial_ordering。實(shí)現(xiàn)很簡單:
std::partial_ordering SpreadsheetCell::operator<=>(const SpreadsheetCell& rhs) const {
return getValue() <=> rhs.getValue();
}
通過實(shí)現(xiàn)operator<=>,C++20會(huì)自動(dòng)為>、`<、<=和>=提供支持,通過將使用這些運(yùn)算符的表達(dá)式重寫為使用<=>的表達(dá)式。例如,類似于myCell<aThirdCell的表達(dá)式會(huì)自動(dòng)重寫為類似于std::is_ lt(myCell<=>aThirdCell)的東西,其中is_lt()是一個(gè)命名比較函數(shù);所以,通過只實(shí)現(xiàn)operator==和operator<=>`,SpreadsheetCell類支持完整的比較運(yùn)算符集:
if (myCell < aThirdCell) {
// ...
}
if (aThirdCell < myCell) {
// ...
}
if (myCell <= aThirdCell) {
// ...
}
if (aThirdCell <= myCell) {
// ...
}
if (myCell > aThirdCell) {
// ...
}
if (aThirdCell > myCell) {
// ...
}
if (myCell >= aThirdCell) {
// ...
}
if (aThirdCell >= myCell) {
// ...
}
if (myCell == aThirdCell) {
// ...
}
if (aThirdCell == myCell) {
// ...
}
if (myCell != aThirdCell) {
// ...
}
if (aThirdCell != myCell) {
// ...
}
由于SpreadsheetCell類支持從雙精度到SpreadsheetCell的隱式轉(zhuǎn)換,因此也支持以下比較:
if (myCell < 10) {
}
if (10 < myCell) {
}
if (10 != myCell) {
}
就像比較兩個(gè)SpreadsheetCell對象一樣,編譯器會(huì)將這些表達(dá)式重寫為使用operator==和<=>的形式,并根據(jù)需要交換參數(shù)的順序。例如,10<myCell首先被重寫為類似于is_lt(10<=>myCell)的東西,這不會(huì)起作用,因?yàn)槲覀冎挥?lt;=>作為成員的重載,這意味著左側(cè)參數(shù)必須是SpreadsheetCell。注意到這一點(diǎn)后,編譯器再嘗試將表達(dá)式重寫為類似于is_gt(myCell<=>10)的東西,這就可以工作了。與以前一樣,如果你想避免隱式轉(zhuǎn)換的輕微性能影響,你可以為雙精度提供特定的重載。而這現(xiàn)在,多虧了C++20,甚至不是很多工作。你只需要提供以下兩個(gè)額外的重載運(yùn)算符作為方法:
[[nodiscard]] bool operator==(double rhs) const;
[[nodiscard]] std::partial_ordering operator<=>(double rhs) const;
這些實(shí)現(xiàn)如下:
bool SpreadsheetCell::operator==(double rhs) const {
return getValue() == rhs;
}
std::partial_ordering SpreadsheetCell::operator<=>(double rhs) const {
return getValue() <=> rhs;
}
2.編譯器生成的比較運(yùn)算符
在查看SpreadsheetCell的operator和<=>的實(shí)現(xiàn)時(shí),可以看到它們只是簡單地比較所有數(shù)據(jù)成員。在這種情況下,我們可以進(jìn)一步減少編寫代碼的行數(shù),因?yàn)镃++20可以為我們完成這些工作。就像可以顯式默認(rèn)化拷貝構(gòu)造函數(shù)一樣,operator和<=>也可以被默認(rèn)化,這種情況下編譯器將為你編寫它們,并通過依次比較每個(gè)數(shù)據(jù)成員來實(shí)現(xiàn)它們。此外,如果你只顯式默認(rèn)化operator<=>,編譯器還會(huì)自動(dòng)包含一個(gè)默認(rèn)的operator。因此,對于沒有顯式operator和<=>用于雙精度的SpreadsheetCell版本,我們可以簡單地編寫以下單行代碼,為比較兩個(gè)SpreadsheetCell添加對所有六個(gè)比較運(yùn)算符的完全支持:
[[nodiscard]] std::partial_ordering operator<=>(const SpreadsheetCell&) const = default;
此外,你可以將operator<=>的返回類型使用auto,這種情況下編譯器會(huì)基于數(shù)據(jù)成員的<=>運(yùn)算符的返回類型來推斷返回類型。如果你的類有不支持operator<=>的數(shù)據(jù)成員,那么返回類型推斷將不起作用,你需要顯式指定返回類型為strong_ordering、partial_ordering或weak_ordering。為了讓編譯器能夠編寫默認(rèn)的<=>運(yùn)算符,類的所有數(shù)據(jù)成員都需要支持operator<=>,這種情況下返回類型可以是auto,或者是operator<和==,這種情況下返回類型不能是auto。由于SpreadsheetCell有一個(gè)雙精度數(shù)據(jù)成員,編譯器推斷返回類型為partial_ordering。
[[nodiscard]] auto operator<=>(const SpreadsheetCell&) const = default;
單獨(dú)的顯式默認(rèn)化的operator<=>適用于沒有顯式operator==和<=>用于雙精度的SpreadsheetCell版本。如果你添加了這些顯式的雙精度版本,你就添加了一個(gè)用戶聲明的operator==(double)。因?yàn)檫@個(gè)原因,編譯器將不再自動(dòng)生成operator==(const SpreadsheetCell&),所以你必須自己顯式默認(rèn)化一個(gè),如下所示:
export class SpreadsheetCell {
public:
// Omitted for brevity
[[nodiscard]] auto operator<=>(const SpreadsheetCell&) const = default;
[[nodiscard]] bool operator==(const SpreadsheetCell&) const = default;
[[nodiscard]] bool operator==(double rhs) const;
[[nodiscard]] std::partial_ordering operator<=>(double rhs) const;
// Omitted for brevity
};
如果你的類可以顯式默認(rèn)化operator<=>,我建議這樣做,而不是自己實(shí)現(xiàn)它。通過讓編譯器為你編寫,它將隨著新添加或修改的數(shù)據(jù)成員保持最新狀態(tài)。如果你自己實(shí)現(xiàn)了運(yùn)算符,那么每當(dāng)你添加數(shù)據(jù)成員或更改現(xiàn)有數(shù)據(jù)成員時(shí),你都需要記得更新你的operator<=>實(shí)現(xiàn)。如果operator==沒有被編譯器自動(dòng)生成,同樣的規(guī)則也適用于它。只有當(dāng)它們作為參數(shù)有對類類型的引用時(shí),才能顯式默認(rèn)化operator==和<=>。例如,以下不起作用:
[[nodiscard]] auto operator<=>(double) const = default; // 不起作用!
注意:要在C++20中向類添加對所有六個(gè)比較運(yùn)算符的支持: ? 如果默認(rèn)化的operator<=>適用于你的類,那么只需要一行代碼顯式默認(rèn)化operator<=>作為方法即可。在某些情況下,你可能需要顯式默認(rèn)化operator==。 ? 否則,只需重載并實(shí)現(xiàn)operator==和<=>作為方法。無需手動(dòng)實(shí)現(xiàn)其他比較運(yùn)算符。