方法重载
你可能已经注意到,你可以在一个类中写多个构造函数,所有这些构造函数都有相同的名字。这些构造函数只在参数的数量和/或类型上有所不同。你可以对C++中的任何方法或函数做同样的事情。具体来说,你可以通过为具有不同数量和/或类型的参数的多个函数使用同一个名称来重载一个函数或方法。例如,在SpreadsheetCell类中,你可以将setString()和setValue()都重命名为set()。类定义现在看起来像这样:
export class SpreadsheetCell {
public:
void set(double value);
void set(std::string_view value);
// 省略了一些内容以保持简洁
};
set()方法的实现保持不变。当你编写代码调用set()时,编译器会根据你传递的参数来确定调用哪个实例:如果你传递一个string_view,编译器会调用string_view实例;如果你传递一个double,编译器会调用double实例。这被称为重载解析。
你可能会试图对getValue()和getString()做同样的事情:将它们都重命名为get()。然而,这样做是不行的。C++不允许你仅基于方法的返回类型来重载一个方法名,因为在许多情况下,编译器无法确定你试图调用的是哪个方法实例。例如,如果方法的返回值没有被捕获在任何地方,编译器就没有办法知道你试图调用的是哪个方法实例。
基于const的重载
你可以基于const来重载一个方法。也就是说,你可以写两个具有相同名称和相同参数的方法,一个声明为const,另一个则不是。如果你有一个const对象,编译器会调用const方法;如果你有一个非const对象,它会调用非const重载。通常,const重载和非const重载的实现是相同的。为了避免代码重复,你可以使用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重载实现为你通常会做的那样,并通过适当的转换将非const重载的调用转发给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&)转换为const Spreadsheet&。接下来,你调用getCellAt()的const重载,它返回一个const SpreadsheetCell&。然后你用const_cast()将这个转换为非const SpreadsheetCell&。
有了这两个getCellAt()的重载,你现在可以在const和非const Spreadsheet对象上调用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重载转发给const重载可以避免重复那些代码。
显式删除重载
重载的方法可以被显式删除,这使你能够禁止使用特定参数调用某个方法。例如,SpreadsheetCell类有一个setValue(double)方法,可以这样调用:
SpreadsheetCell cell;
cell.setValue(1.23);
cell.setValue(123);
对于第三行,编译器将整数值(123)转换为double,然后调用setValue(double)。如果由于某种原因,你不希望setValue()使用整数调用,你可以显式删除setValue()的整数重载:
export class SpreadsheetCell {
public:
void setValue(double value);
void setValue(int) = delete;
};
有了这个改变,尝试使用整数调用setValue()的操作将被编译器标记为错误。
Ref-Qualified方法
普通类方法可以在非临时和临时类实例上调用。假设你有以下类:
class TextHolder {
public:
TextHolder(string text) : m_text { move(text) } {}
const string& getText() const { return m_text; }
private:
string m_text;
};
当然,毫无疑问,你可以在非临时实例的TextHolder上调用getText()方法。这里有一个例子:
TextHolder textHolder { "Hello world!" };
cout << textHolder.getText() << endl;
然而,getText()也可以在临时实例上调用:
cout << TextHolder{ "Hello world!" }.getText() << endl;
cout << move(textHolder).getText() << endl;
你可以通过添加所谓的ref-qualifier来明确指定可以在哪种类型的实例上调用某个方法,无论是临时的还是非临时的。如果一个方法只应该在非临时实例上调用,在方法头后加上&限定符。类似地,如果一个方法只应该在临时实例上调用,在方法头后加上&&限定符。
下面修改后的TextHolder类实现了带有&限定符的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;
};
假设你有以下调用:
TextHolder textHolder { "Hello world!" };
cout << textHolder.getText() << endl;
cout << TextHolder{ "Hello world!" }.getText() << endl;
cout << move(textHolder).getText() << endl;
那么第一次调用getText()会调用带有&限定符的重载,而第二次和第三次调用则会调用带有&&限定符的重载。
内联方法
C++允许你建议调用一个方法(或函数)时,不应该在生成的代码中实际实现为调用一个单独的代码块。相反,编译器应该将方法的主体直接插入到调用该方法的代码中。这个过程被称为内联,希望这种行为的方法被称为内联方法。
你可以通过在方法定义中的名字前放置inline关键字来指定一个内联方法。例如,你可能想让SpreadsheetCell类的访问器方法成为内联的,这种情况下,你会这样定义它们:
inline double SpreadsheetCell::getValue() const {
m_numAccesses++;
return m_value;
}
inline std::string SpreadsheetCell::getString() const {
m_numAccesses++;
return doubleToString(m_value);
}
这向编译器提供了一个提示,用实际的方法体替换对getValue()和getString()的调用,而不是生成代码来进行函数调用。请注意,inline关键字只是一个提示给编译器。如果编译器认为这会影响性能,它可以忽略它。
有一个注意事项:内联方法(和函数)的定义必须在每个调用它们的源文件中都可用。如果你想一下,这是有道理的:如果编译器看不到方法定义,它怎么能代替方法的主体呢?因此,如果你编写内联方法,你应该将这些方法的定义放在类定义所在的同一个文件中。
注意,高级C++编译器不要求你将内联方法的定义放在类定义的同一个文件中。例如,Microsoft Visual C++支持链接时代码生成(LTCG),它会自动内联小的函数体,即使它们没有被声明为inline,即使它们没有定义在类定义的同一个文件中。GCC和Clang也有类似的功能。
在C++20模块之外,如果一个方法的定义直接放在类定义中,即使没有使用inline关键字,该方法也隐式地被标记为内联。使用C++20中从模块导出的类时,情况并非如此。如果你希望这些方法是内联的,你需要用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);
}
// 省略了一些内容以保持简洁
}
注意,如果你在调试器中单步执行一个被内联的函数调用,一些高级C++调试器会跳转到内联函数的实际源代码,给你造成了函数调用的假象,而实际上代码是内联的。许多C++程序员在不理解将一个方法标记为内联的后果时,就使用了内联方法语法。将一个方法或函数标记为内联只是给编译器一个提示。编译器只会内联最简单的方法和函数。如果你定义了一个编译器不想内联的内联方法,它会默默地忽略这个提示。现代编译器会在决定内联一个方法或函数之前,考虑诸如代码膨胀等指标,并且不会内联任何不划算的东西。
默认参数
在C++中,与方法重载类似的功能是默认参数。你可以在原型中为函数和方法参数指定默认值。如果用户为这些参数提供了参数,那么默认值将被忽略。如果用户省略了这些参数,将使用默认值。不过,有一个限制:你只能为从最右边的参数开始的连续参数列表提供默认值。否则,编译器将无法将缺失的参数与默认参数匹配。默认参数可用于函数、方法和构造函数。例如,你可以为Spreadsheet构造函数中的宽度和高度分配默认值,如下所示:
export class Spreadsheet {
public:
Spreadsheet(size_t width = 100, size_t height = 100);
// 省略了一些内容以保持简洁
};
Spreadsheet构造函数的实现保持不变。请注意,你只在方法声明中指定默认参数,而不是在定义中指定。现在,尽管只有一个非复制构造函数,你仍然可以使用零个、一个或两个参数调用Spreadsheet构造函数:
Spreadsheet s1;
Spreadsheet s2 { 5 };
Spreadsheet s3 { 5, 6 };
一个为所有参数提供默认值的构造函数可以作为默认构造函数。也就是说,你可以在不指定任何参数的情况下构造该类的对象。如果你尝试同时声明一个默认构造函数和一个为所有参数提供默认值的多参数构造函数,编译器会报错,因为如果你不指定任何参数,它不知道该调用哪个构造函数。