学完Efficient c++ (24-25)

27 篇文章 0 订阅
24 篇文章 0 订阅

条款24:若所有参数皆需类型转换,请为此采用non-member函数

这个条款告诉了我们操作符重载被重载为成员函数和非成员函数的区别。作者想给我们提个醒,如果我们在使用操作符时希望操作符的任意操作数都可能发生隐式类型转换那么应该把该操作符重载成非成员函数。

我们首先说明:如果一个操作符是成员函数,那么它的第一个操作数(即调用对象)不会发生隐式类型转换

现在我们有一个Rational类,并且它可以和int隐式转换:

class Rational {
public:
    Rational(int numerator = 0, int denominator = 1);
    ...
};

首先简单讲解一下当操作符被重载成员函数时,第一个操作数特殊的身份。操作符一旦被设计为成员函数,它在被使用时的特殊性就显现出来了——单从表达式你无法直接看出是类的哪个对象在调用这个操作符函数,不是吗?例如下方的有理数类重载的操作符”+”,当我们在调用Rational z = x + y;时,调用操作符函数的对象并没有直接显示在代码中——这个操作符的this指针指向x还是y呢?

class Rational {
public:
  //...
  const Rational operator+(const Rational& rhs) const; 
pricate:
  //...
}

作为成员函数的操作符的第一个隐形参数”this指针”总是指向第一个操作数,所以上边的调用也可以写成Rational z = x.operator+(y);,这就是操作符的更像函数的调用方法。那么,做为成员函数的操作符默认操作符的第一个操作数应当是正确的类对象——编译器正式根据第一个操作数的类型来确定被调用的操作符到底属于哪一个类的。因而第一个操作数是不会发生隐式类型转换的,第一个操作数是什么类型,它就调用那个类型对应的操作符。

我们举例说明:当Ratinoal类的构造函数允许int类型隐式转换为Rational类型时,Rational z = x + 2;是可以通过编译的,因为操作符是被Rational类型的x调用,同时将2隐式转换为Ratinoal类型,完成加法。但是Rational z = 2 + x;却会引发编译器报错,因为由于操作符的第一个操作数不会发生隐式类型转换,所以加号“+”实际上调用的是2—一个int类型的操作符,因此编译器会试图将Rational类型的x转为int,这样是行不通的。

因此在你编写诸如加减乘除之类的(但不限于这些)操作符、并假定允许每一个操作数都发生隐式类型转换时,请不要把操作符函数重载为成员函数。因为当第一个操作数不是正确类型时,可能会引发调用的失败。解决方案是,请将操作符声明为类外的非成员函数,你可以选择友元让操作符内的运算更便于进行,也可以为私有成员封装更多接口来保证操作符的实现,这都取决于你的选择。

const Rational operator+(const Rational& lhs, const Rational& rhs);

希望这一条款能解释清楚操作符在作为成员函数与非成员函数时的区别。此条款并没有明确说明该法则只适用于操作符,但是除了操作符外,我实在想不到更合理的用途了。

注:如果你想禁止隐式类型转换的发生,请把你每一个单参数构造函数后加上关键字explicit

条款 25:考虑写出一个不抛异常的swap函数

由于std::swap函数在 C++11 后改为用std::move实现,因此几乎已经没有性能的缺陷,也不再有像原书中所说的为自定义类型去自己实现的必要。不过原书中透露的思想还是值得一学的。

如果想为自定义类型实现自己的swap方法,可以考虑使用模板全特化,并且STL也是这种做法(public swap成员函数和std::swap特化版本):

class WidgetImpl{
public:
    ...
private:
    int a, b, c;
    std::vector<double> v;
    ...
}
class Widget {
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs){
        ...
        *pImpl = *(rhs.pImpl);
        ...
    }

    void swap(Widget& other) {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
    ...

private:
    WidgetImpl* pImpl;
};

namespace std {
    template<>
    void swap<Widget>(Widget& a, Widget& b) {
        a.swap(b);
    }
}

注意,由于外部函数并不能直接访问Widget的private成员变量pImpl,因此我们先是在类中定义了一个 public 成员函数,再由std::swap去调用这个成员函数。

WidgetWidgetImpl是类模板,情况就没有这么简单了,因为 C++ 不支持函数模板偏特化,所以只能使用重载的方式:

template<typename T>
class WidgetImpl {...};

template<typename T>
class Widget {...};
namespace std {
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) {
        a.swap(b);
    }
}

但很抱歉,这种做法是被 STL 禁止的,因为这是在试图向 STL 中添加新的内容(templates),所以我们只能退而求其次,在其它命名空间中定义新的swap函数:

namespace WidgetStuff {
    ...
    template<typename T>
    class Widget { ... };
    ...
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) {
        a.swap(b);
    }
}

我们希望在对自定义对象进行操作时找到正确的swap函数重载版本,这时候如果再写成std::swap,就会强制使用 STL 中的swap函数,无法满足我们的需求,因此需要改写成:

using std::swap;
swap(obj1, obj2);

这样,C++ 名称查找法则能保证我们优先使用的是自定义的swap函数而非 STL 中的swap函数。

C++ 名称查找法则:编译器会从使用名字的地方开始向上查找,由内向外查找各级作用域(命名空间)直到全局作用域(命名空间),找到同名的声明即停止,若最终没找到则报错。 函数匹配优先级:普通函数 > 特化函数 > 模板函数

  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值