ator() const;
private:
...
};
class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};
// 于是乎可以轻松实现乘法
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // 没问题
result = result * oneEighth; // 没问题
到目前为止还没有实现致命问题,然而:
result = oneHalf * 2; // ok!
result = 2 * oneHalf; // error!
// result = 2.operator*(oneHalf); of course wrong!
第一个式子能够成立,是因为实现了隐式类型转换。编译器知道你在传递一个int,而函数需要的是rational,但它也知道只要调用Rational构造函数并赋予你所提供的int,就可以变出一个适当的rational出来,于是就这么做了。相当于:
const Rational temp(2);
result = oneHalf * temp;
当然这只涉及non-explicit构造函数,才能这么做。如果是explicit构造函数,这个语句无法通过编译。
问题解决
result = oneHalf * 2; // ok!
result = 2 * oneHalf; // error!
只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”-即this对象-那个隐喻参数,绝不是隐式转换的合格参与者。这就是为什么语句1能够通过编译而语句2不可以。
于是,方法就是,让operator*称为一个non-member函数,允许编译器在每一实参身上执行隐式类型转换。
const Rational operator*(const Rational& lhs, const Rational& rhs) {
...
}
Rational oneFourth(1, 4);
Rational result = oneFourth * 2; // right!
result = 2 * oneFourth; // right!
补充思考:
是否应该把该operator*声明为friend?
答案是否定的!请注意,member的反面不是friend,而是non-member!在此代码中,operator*完全可以借由rational的public接口完成任务,于是便不必把他声明为friend。无论何时,如果可以避免friend函数就应该避免。
总结:
如果你需要为某个函数的所有参数(包括this)进行类型转换,那么这个函数必须是个non-member。
8. 考虑如何写出特化的swap函数
swap作为STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。由于此函数如此有用,也意味着他具有非凡哥的复杂度。本节谈论这些复杂度以及相应处理。
问题产生1
namespace std {
template
void swap(T &a, T &b) { T temp(a); a = b; b = temp; } }
这是标准程序库提供的swap算法。非常地简单,只要T有copying相关操作即可。然而这个算法对于有些情况却显得不那么高效。例如,在处理“以指针指向一个对象,内含真正数据”的那种类型。(这种设计的常见形式是所谓“pimpl手法:pointer to implemention)
class WidgetImpl { // 实现细节不重要。
public: // 针对Widget设计的class
...
private:
int a, b, c;
std::vector
v; ... }; class Widget { public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) { ... *pImpl = *(rhs.pImpl); ... } private: WidgetImpl* pImpl; };
对此类调用算法库的swap就会非常低效。因为他总共要复制三个Widget和三个WidgetImpl对象!而事实上,只需要改变指针的指向就可以了。
问题解决1
我们可能尝试用以下方法解决,让swap针对Widget特化。
尝试一:
namespace std {
template<> // 表示他是std::swap的一个全特化
void swap
(Widget &a, Widget &b) { swap(a.pImpl, b.pImpl); } }
通常来说,我们是不能够改变std命名空间内的任何东西,但可以(被允许)为标准template制造特化版本的。
但实际上这个是无法通过编译的。因为他企图调用class的私有成员。
所以更合理的做法,是令他调用成员函数。
解法:
class Widget {
public:
...
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pInmpl);
}
...
};
private:
WidgetImpl* pImpl;
};
namespace std {
template<>
void swap
(Widget &a, Widget &b) { a.swap(b); } }
这个做法不仅能够通过编译,而且与STL容器有一致性。
问题产生2
假设Widget和WidgetImpl都是class template而非class,也许我们可以试试把WidgetImpl内的数据类型加以参数化:
template
class WidgetImpl {...}; template
class Widget {...}; // 在Widget里面放入swap成员函数就像以往一样简单 // 但在写特化std::swap时出现了问题 namespace std { template
void swap< Widget
> (Widget
& a, Widget
& b) { a.swap(b); } }
以上特化swap其实有问题的。我们企图偏特化这个function template,但C++只允许对class template偏特化。(随后会介绍全特化和偏特化)。当你尝试偏特化一个function template时,更常见的做法是添加重载函数:
namespace std {
template
void swap(Widget
& a, Widget
& b) { a.swap(b); } }
但实际上,这也是不行的!因为std是个特殊的命名空间,其管理规则比较特殊,客户可以全特化std内的template,但不可以添加新的template到std里面。
问题解决2
解决这个问题的方法就是,声明一个non-member swap让它调用member swap,但不在将那个non-member swap声明为std::swap特化版或重载版本。
namespace WidgetStuff {
template