深入理解C++方法重載、內(nèi)聯(lián)與高級用法
方法重載
你可能已經(jīng)注意到,你可以在一個類中寫多個構造函數(shù),所有這些構造函數(shù)都有相同的名字。這些構造函數(shù)只在參數(shù)的數(shù)量和/或類型上有所不同。你可以對C++中的任何方法或函數(shù)做同樣的事情。具體來說,你可以通過為具有不同數(shù)量和/或類型的參數(shù)的多個函數(shù)使用同一個名稱來重載一個函數(shù)或方法。例如,在SpreadsheetCell類中,你可以將setString()和setValue()都重命名為set()。類定義現(xiàn)在看起來像這樣:
export class SpreadsheetCell {
public:
void set(double value);
void set(std::string_view value);
// 省略了一些內(nèi)容以保持簡潔
};
set()方法的實現(xiàn)保持不變。當你編寫代碼調(diào)用set()時,編譯器會根據(jù)你傳遞的參數(shù)來確定調(diào)用哪個實例:如果你傳遞一個string_view,編譯器會調(diào)用string_view實例;如果你傳遞一個double,編譯器會調(diào)用double實例。這被稱為重載解析。
你可能會試圖對getValue()和getString()做同樣的事情:將它們都重命名為get()。然而,這樣做是不行的。C++不允許你僅基于方法的返回類型來重載一個方法名,因為在許多情況下,編譯器無法確定你試圖調(diào)用的是哪個方法實例。例如,如果方法的返回值沒有被捕獲在任何地方,編譯器就沒有辦法知道你試圖調(diào)用的是哪個方法實例。
基于const的重載
你可以基于const來重載一個方法。也就是說,你可以寫兩個具有相同名稱和相同參數(shù)的方法,一個聲明為const,另一個則不是。如果你有一個const對象,編譯器會調(diào)用const方法;如果你有一個非const對象,它會調(diào)用非const重載。通常,const重載和非const重載的實現(xiàn)是相同的。為了避免代碼重復,你可以使用Scott Meyer的const_cast()模式。
例如,Spreadsheet類有一個名為getCellAt()的方法,返回對非const SpreadsheetCell的引用。你可以添加一個const重載,返回對const SpreadsheetCell的引用,如下所示:
export class Spreadsheet {
public:
SpreadsheetCell& getCellAt(size_t x, size_t y);
const SpreadsheetCell& getCellAt(size_t x, size_t y) const;
// 代碼省
Scott Meyer的const_cast()模式將const重載實現(xiàn)為你通常會做的那樣,并通過適當?shù)霓D(zhuǎn)換將非const重載的調(diào)用轉(zhuǎn)發(fā)給const重載,如下所示:
const SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y) const {
verifyCoordinate(x, y);
return m_cells[x][y];
}
SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y) {
return const_cast<SpreadsheetCell&>(as_const(*this).getCellAt(x, y));
}
基本上,你首先使用std::as_const()(定義在<utility>中)將*this(一個Spreadsheet&)轉(zhuǎn)換為const Spreadsheet&。接下來,你調(diào)用getCellAt()的const重載,它返回一個const SpreadsheetCell&。然后你用const_cast()將這個轉(zhuǎn)換為非const SpreadsheetCell&。
有了這兩個getCellAt()的重載,你現(xiàn)在可以在const和非const Spreadsheet對象上調(diào)用getCellAt():
Spreadsheet sheet1 { 5, 6 };
SpreadsheetCell& cell1 { sheet1.getCellAt(1, 1) };
const Spreadsheet sheet2 { 5, 6 };
const SpreadsheetCell& cell2 { sheet2.getCellAt(1, 1) };
在這種情況下,const重載的getCellAt()并沒有做太多的事情,所以你通過使用const_cast()模式并沒有贏得太多。然而,想象一下,如果const重載的getCellAt()做了更多的工作;那么將非const重載轉(zhuǎn)發(fā)給const重載可以避免重復那些代碼。
顯式刪除重載
重載的方法可以被顯式刪除,這使你能夠禁止使用特定參數(shù)調(diào)用某個方法。例如,SpreadsheetCell類有一個setValue(double)方法,可以這樣調(diào)用:
SpreadsheetCell cell;
cell.setValue(1.23);
cell.setValue(123);
對于第三行,編譯器將整數(shù)值(123)轉(zhuǎn)換為double,然后調(diào)用setValue(double)。如果由于某種原因,你不希望setValue()使用整數(shù)調(diào)用,你可以顯式刪除setValue()的整數(shù)重載:
export class SpreadsheetCell {
public:
void setValue(double value);
void setValue(int) = delete;
};
有了這個改變,嘗試使用整數(shù)調(diào)用setValue()的操作將被編譯器標記為錯誤。
Ref-Qualified方法
普通類方法可以在非臨時和臨時類實例上調(diào)用。假設你有以下類:
class TextHolder {
public:
TextHolder(string text) : m_text { move(text) } {}
const string& getText() const { return m_text; }
private:
string m_text;
};
當然,毫無疑問,你可以在非臨時實例的TextHolder上調(diào)用getText()方法。這里有一個例子:
TextHolder textHolder { "Hello world!" };
cout << textHolder.getText() << endl;
然而,getText()也可以在臨時實例上調(diào)用:
cout << TextHolder{ "Hello world!" }.getText() << endl;
cout << move(textHolder).getText() << endl;
你可以通過添加所謂的ref-qualifier來明確指定可以在哪種類型的實例上調(diào)用某個方法,無論是臨時的還是非臨時的。如果一個方法只應該在非臨時實例上調(diào)用,在方法頭后加上&限定符。類似地,如果一個方法只應該在臨時實例上調(diào)用,在方法頭后加上&&限定符。
下面修改后的TextHolder類實現(xiàn)了帶有&限定符的getText(),通過返回對m_text的引用。而帶有&&限定符的getText()返回m_text的右值引用,這樣m_text就可以從TextHolder中移動出來。如果你想從臨時TextHolder實例中檢索文本,這可能會更有效率。
class TextHolder {
public:
TextHolder(string text) : m_text { move(text) } {}
const string& getText() const & { return m_text; }
string&& getText() && { return move(m_text); }
private:
string m_text;
};
假設你有以下調(diào)用:
TextHolder textHolder { "Hello world!" };
cout << textHolder.getText() << endl;
cout << TextHolder{ "Hello world!" }.getText() << endl;
cout << move(textHolder).getText() << endl;
那么第一次調(diào)用getText()會調(diào)用帶有&限定符的重載,而第二次和第三次調(diào)用則會調(diào)用帶有&&限定符的重載。
內(nèi)聯(lián)方法
C++允許你建議調(diào)用一個方法(或函數(shù))時,不應該在生成的代碼中實際實現(xiàn)為調(diào)用一個單獨的代碼塊。相反,編譯器應該將方法的主體直接插入到調(diào)用該方法的代碼中。這個過程被稱為內(nèi)聯(lián),希望這種行為的方法被稱為內(nèi)聯(lián)方法。
你可以通過在方法定義中的名字前放置inline關鍵字來指定一個內(nèi)聯(lián)方法。例如,你可能想讓SpreadsheetCell類的訪問器方法成為內(nèi)聯(lián)的,這種情況下,你會這樣定義它們:
inline double SpreadsheetCell::getValue() const {
m_numAccesses++;
return m_value;
}
inline std::string SpreadsheetCell::getString() const {
m_numAccesses++;
return doubleToString(m_value);
}
這向編譯器提供了一個提示,用實際的方法體替換對getValue()和getString()的調(diào)用,而不是生成代碼來進行函數(shù)調(diào)用。請注意,inline關鍵字只是一個提示給編譯器。如果編譯器認為這會影響性能,它可以忽略它。
有一個注意事項:內(nèi)聯(lián)方法(和函數(shù))的定義必須在每個調(diào)用它們的源文件中都可用。如果你想一下,這是有道理的:如果編譯器看不到方法定義,它怎么能代替方法的主體呢?因此,如果你編寫內(nèi)聯(lián)方法,你應該將這些方法的定義放在類定義所在的同一個文件中。
注意,高級C++編譯器不要求你將內(nèi)聯(lián)方法的定義放在類定義的同一個文件中。例如,Microsoft Visual C++支持鏈接時代碼生成(LTCG),它會自動內(nèi)聯(lián)小的函數(shù)體,即使它們沒有被聲明為inline,即使它們沒有定義在類定義的同一個文件中。GCC和Clang也有類似的功能。
在C++20模塊之外,如果一個方法的定義直接放在類定義中,即使沒有使用inline關鍵字,該方法也隱式地被標記為內(nèi)聯(lián)。使用C++20中從模塊導出的類時,情況并非如此。如果你希望這些方法是內(nèi)聯(lián)的,你需要用inline關鍵字標記它們。這里有一個例子:
export class SpreadsheetCell {
public:
inline double getValue() const {
m_numAccesses++;
return m_value;
}
inline std::string getString() const {
m_numAccesses++;
return doubleToString(m_value);
}
// 省略了一些內(nèi)容以保持簡潔
}
注意,如果你在調(diào)試器中單步執(zhí)行一個被內(nèi)聯(lián)的函數(shù)調(diào)用,一些高級C++調(diào)試器會跳轉(zhuǎn)到內(nèi)聯(lián)函數(shù)的實際源代碼,給你造成了函數(shù)調(diào)用的假象,而實際上代碼是內(nèi)聯(lián)的。許多C++程序員在不理解將一個方法標記為內(nèi)聯(lián)的后果時,就使用了內(nèi)聯(lián)方法語法。將一個方法或函數(shù)標記為內(nèi)聯(lián)只是給編譯器一個提示。編譯器只會內(nèi)聯(lián)最簡單的方法和函數(shù)。如果你定義了一個編譯器不想內(nèi)聯(lián)的內(nèi)聯(lián)方法,它會默默地忽略這個提示?,F(xiàn)代編譯器會在決定內(nèi)聯(lián)一個方法或函數(shù)之前,考慮諸如代碼膨脹等指標,并且不會內(nèi)聯(lián)任何不劃算的東西。
默認參數(shù)
在C++中,與方法重載類似的功能是默認參數(shù)。你可以在原型中為函數(shù)和方法參數(shù)指定默認值。如果用戶為這些參數(shù)提供了參數(shù),那么默認值將被忽略。如果用戶省略了這些參數(shù),將使用默認值。不過,有一個限制:你只能為從最右邊的參數(shù)開始的連續(xù)參數(shù)列表提供默認值。否則,編譯器將無法將缺失的參數(shù)與默認參數(shù)匹配。默認參數(shù)可用于函數(shù)、方法和構造函數(shù)。例如,你可以為Spreadsheet構造函數(shù)中的寬度和高度分配默認值,如下所示:
export class Spreadsheet {
public:
Spreadsheet(size_t width = 100, size_t height = 100);
// 省略了一些內(nèi)容以保持簡潔
};
Spreadsheet構造函數(shù)的實現(xiàn)保持不變。請注意,你只在方法聲明中指定默認參數(shù),而不是在定義中指定。現(xiàn)在,盡管只有一個非復制構造函數(shù),你仍然可以使用零個、一個或兩個參數(shù)調(diào)用Spreadsheet構造函數(shù):
Spreadsheet s1;
Spreadsheet s2 { 5 };
Spreadsheet s3 { 5, 6 };
一個為所有參數(shù)提供默認值的構造函數(shù)可以作為默認構造函數(shù)。也就是說,你可以在不指定任何參數(shù)的情況下構造該類的對象。如果你嘗試同時聲明一個默認構造函數(shù)和一個為所有參數(shù)提供默認值的多參數(shù)構造函數(shù),編譯器會報錯,因為如果你不指定任何參數(shù),它不知道該調(diào)用哪個構造函數(shù)。