Item 23: 用非成员非友元函数取代成员函数
想象一个象征 web 浏览器的类。在大量的函数中,这样一个类也许会提供清空已下载成分的缓存。清空已访问 URLs 的历史,以及从系统移除所有 cookies 的功能:
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
很多用户希望能一起执行全部这些动作,所以 WebBrowser 可能也会提供一个函数去这样做:
class WebBrowser {
public:
...
void clearEverything(); // calls clearCache, clearHistory,
// and removeCookies
...
};
当然,这个功能也能通过非成员函数调用适当的成员函数来提供:
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
那么哪个更好呢,成员函数 clearEverything 还是非成员函数 clearBrowser?
从封装性来说,因为clearBrowser不能访问类的任何私有成员,因此clearBrowser(非成员非友元函数)比 clearEverything(成员函数)更可取:它能为 WebBrowser 获得更强的封装性。
可以采用以下的方式来定义:
namespace WebBrowserStuff {
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}
将clearBrowser和class WebBrowser放在同一个名字空间内。
一个类似 WebBrowser 的类可以有大量的方便性函数,一些是书签相关的,另一些打印相关的,还有一些是 cookie 管理相关的,等等。作为一个一般的惯例,多数客户仅对这些方便性函数的集合中的一些感兴趣。没有理由让一个只对书签相关的方便性函数感兴趣的客户在编译时依赖其它函数,例如,cookie 相关的方便性函数。分隔它们的直截了当的方法就是在一个头文件中声明书签相关的方便性函数,在另一个不同的头文件中声明 cookie 相关的方便性函数,在第三个头文件声明打印相关的方便性函数,等等:
// header "webbrowser.h" - header for class WebBrowser itself
// as well as "core" WebBrowser-related functionality
namespace WebBrowserStuff {
class WebBrowser { ... };
... // "core" related functionality, e.g.
// non-member functions almost
// all clients need
}
// header "webbrowserbookmarks.h"
namespace WebBrowserStuff {
... // bookmark-related convenience
} // functions
// header "webbrowsercookies.h"
namespace WebBrowserStuff {
... // cookie-related convenience
} // functions
将所有方便性函数放入多个头文件中——但是在一个 namespace 中——也意味着客户能容易地扩充方便性函数的集合。他们必须做的全部就是在 namespace 中加入更多的非成员非友元函数。
总结:
用非成员非友元函数取代成员函数。这样做可以提高封装性,包裹弹性,和机能扩充性。
Item 24: 当所有参数皆需类型转换,请为此采用非成员函数
设计一个用来表现有理数的类
class Rational {
public:
Rational(int numerator = 0, // ctor is deliberately not explicit;
int denominator = 1); // allows implicit int-to-Rational
// conversions
int numerator() const; // accessors for numerator and
int denominator() const; // denominator - see Item 22
private:
...
};
研究一下让 operator* 成为 Rational 的一个成员函数的想法究竟如何:
class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};
这个设计让你在有理数相乘时不费吹灰之力:
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // fine
result = result * oneEighth; // fine
当你试图做混合模式的算术运算时,可是,你发现只有一半时间它能工作:
result = oneHalf * 2; // fine
result = 2 * oneHalf; // error!
当你重写最后两个例子为功能等价的另一种形式时,问题的来源就变得很明显了:
result = oneHalf.operator*(2); // fine
result = 2.operator*(oneHalf); // error!
对象 oneHalf 是一个包含 operator* 的类的实例,所以编译器调用那个函数。然而,整数 2 与类没有关系,因而没有 operator* 成员函数。编译器同样要寻找能如下调用的非成员的 operator*s(也就是说,在 namespace 或全局范围内的 operator*s):
result = operator*(2, oneHalf); // error!
但是在本例中,没有非成员的持有一个 int 和一个 Rational 的 operator*,所以搜索失败。
当然,编译器这样做仅仅是因为提供了一个非显性的构造函数。如果 Rational 的构造函数是显性的,这些语句都将无法编译:
result = oneHalf * 2; // error! (with explicit ctor);
// can't convert 2 to Rational
result = 2 * oneHalf; // same error, same problem
支持混合模式操作失败了,但是至少两个语句的行为将步调一致。
然而,你的目标是既保持一致性又要支持混合运算,也就是说,一个能使上面两个语句都可以编译的设计。让我们返回这两个语句看一看,为什么即使 Rational 的构造函数不是显式的,也是一个可以编译而另一个不行:
result = oneHalf * 2; // fine (with non-explicit ctor)
result = 2 * oneHalf; // error! (even with non-explicit ctor)
其原因在于仅仅当参数列在参数列表中的时候,它们才有资格进行隐式类型转换。而对应于成员函数被调用的那个对象的隐含参数—— this 指针指向的那个——根本没有资格进行隐式转换。这就是为什么第一个调用能编译而第二个不能。第一种情况包括一个参数被列在参数列表中,而第二种情况没有。
你还是希望支持混合运算,然而,现在做到这一点的方法或许很清楚了:让 operator* 作为非成员函数,因此就允许便一起将隐式类型转换应用于所有参数:
class Rational {
... // contains no operator*
};
const Rational operator*(const Rational& lhs, // now a non-member
const Rational& rhs) // function
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // fine
result = 2 * oneFourth; // hooray, it works!
另外,operator* 完全能够根据 Rational 的 public 接口完全实现,因此无需将其声明为友元函数。上面的代码展示了做这件事的方法之一。这导出了一条重要的结论:与成员函数相对的是非成员函数,而不是友元函数。
总结:
如果你需要在一个函数的所有参数(包括被 this 指针所指向的那个)上使用类型转换,这个函数必须是一个非成员函数。