条款24: 在函数重载和设定参数缺省值间慎重选择
会对函数重载和设定参数缺省值产生混淆的原因在于,它们都允许一个函数以多种方式被调用:
void f(); // f被重载
void f(int x);
f(); // 调用f()
f(10); // 调用f(int)
void g(int x = 0); // g 有一个
// 缺省参数值
g(); // 调用g(0)
g(10); // 调用g(10)
那么,什么时候该用哪种方法呢?
答案取决于另外两个问题。第一,确实有那么一个值可以作为缺省吗?第二,要用到多少种算法?一般来说,如果可以选择一个合适的缺省值并且只是用到一种算法,就使用缺省参数(参见条款38)。否则,就使用函数重载。
下面是一个最多可以计算五个int的最大值的函数。这个函数使用了——深呼一口气,看清楚啦——std::numeric_limits<int>::min(),作为缺省参数值。等会儿再进一步介绍这个值,这里先给出函数的代码:
int max(int a,
int b = std::numeric_limits<int>::min(),
int c = std::numeric_limits<int>::min(),
int d = std::numeric_limits<int>::min(),
int e = std::numeric_limits<int>::min())
{
int temp = a > b ? a : b;
temp = temp > c ? temp : c;
temp = temp > d ? temp : d;
return temp > e ? temp : e;
}
现在可以放松了。std::numeric_limits<int>::min()是C++标准库用一种特有的新方法所表示的一个在C里已经定义了的东西,即C在<limits.h>中定义的INT_MIN宏所表示的那个东西——处理你的C++原代码的编译器所产生的int的最小可能值。是的,它的句法背离了C所具有的简洁,但在那些冒号以及其它奇怪的句法背后,是有道理可循的。
假设想写一个函数模板,其参数为固定数字类型,模板产生的函数可以打印用“实例化类型”表示的最小值。这个模板可以这么写:
template<class T>
void printMinimumValue()
{
cout << 表示为T类型的最小值;
}
如果只是借助<limits.h>和<float.h>来写这个函数会觉得很困难,因为不知道T是什么,所以不知道该打印INT_MIN还是DBL_MIN,或其它什么类型的值。
为避开这些困难,标准C++库(见条款49)在头文件<limits> 中定义了一个类模板numeric_limits,这个类模板本身也定义了一些静态成员函数。每个函数返回的是“实例化这个模板的类型”的信息。也就是说,numeric_limits<int>中的函数返回的信息是关于类型int的,numeric_limits<double> 中的函数返回的信息是关于类型double的。numeric_limits中有一个函数叫min,min返回可表示为“实例化类型”的最小值,所以numeric_limits<int>::min()返回的是代表整数类型的最小值。
有了numeric_limits(和标准库中其它东西一样,numeric_limits存在于名字空间std中;numeric_limits本身在头文件<limits>中),写printMinimumValue就可以象下面这样容易:
template<class T>
void printMinimumValue()
{
cout << std::numeric_limits<T>::min();
}
采用基于numeric_limits的方法来表示“类型相关常量”看起来开销很大,其实不然。因为原代码的冗长的语句不会反映到生成的目标代码中。实际上,对numeric_limits的调用根本就不产生任何指令。想知道怎么回事,看看下面,这是numeric_limits<int>::min的一个很简单的实现:
#include <limits.h>
namespace std {
inline int numeric_limits<int>::min() throw ()
{ return INT_MIN; }
}
因为此函数声明为inline,对它的调用会被函数体代替(见条款33)。它只是个INT_MIN,也就是说,它本身仅仅是个简单的“实现时定义的常量”的#define。所以即使本条款开头的那个max函数看起来好象对每个缺省参数进行了函数调用,其实只不过是用了另一种聪明的方法来表示一个类型相关常量而已(本例中常量值为INT_MIN)。象这样一些高效巧妙的应用在C++标准库里俯拾皆是,这可以参考条款49。
回到max 函数上来:最关键的一点是,不管函数的调用者提供几个参数,max计算时采用的是相同(效率很低)的算法。在函数内部任何地方都不用在意哪些参数是“真”的,哪些是缺省值;而且,所选用的缺省值不可能影响到所采用的算法计算的正确性。这就是使用缺省参数值的方案可行的原因。
对很多函数来说,会找不到合适的缺省值。例如,假设想写一个函数来计算最多可达5个int的平均值。这里就不能用缺省参数,因为函数的结果取决于传入的参数的个数:如果传入3个值,就要将总数除以3;如果传入5个值,就要将总数除以5。另外,假如用户没有提供某个参数时,没有一个“神奇的数字”可以作为缺省值,因为所有可能的int都可以是有效参数。这种情况下就别无选择:必须重载函数:
double avg(int a);
double avg(int a, int b);
double avg(int a, int b, int c);
double avg(int a, int b, int c, int d);
double avg(int a, int b, int c, int d, int e);
另一种必须使用重载函数的情况是:想完成一项特殊的任务,但算法取决于给定的输入值。这种情况对于构造函数很常见:“缺省”构造函数是凭空(没有输入)构造一个对象,而拷贝构造函数是根据一个已存在的对象构造一个对象:
// 一个表示自然数的类
class Natural {
public:
Natural(int initValue);
Natural(const Natural& rhs);
private:
unsigned int value;
void init(int initValue);
void error(const string& msg);
};
inline
void Natural::init(int initValue) { value = initValue; }
Natural::Natural(int initValue)
{
if (initValue > 0) init(initValue);
else error("Illegal initial value");
}
inline Natural::Natural(const Natural& x)
{ init(x.value); }
输入为int的构造函数必须执行错误检查,而拷贝构造函数不需要,所以需要两个不同的函数来实现,这就是重载。还请注意,两个函数都必须对新对象赋一个初值。这会导致在两个构造函数里出现重复代码,所以要写一个“包含有两个构造函数公共代码”的私有成员函数init来解决这个问题。这个方法——在重载函数中调用一个“为重载函数完成某些功能”的公共的底层函数——很值得牢记,因为它经常有用(见条款12)。