隐式类型转换与非成员函数(effective C++ 条款24&46)

写在前面

条款24与46都提到了隐式类型转换,给出的例子都是重写版本的’*’运算符,这里简单做个总结。

什么是隐式类型转换

首先,隐式类型转换是编译器自动完成的操作,通常出现在混合表达式中。例如,我们经常使用的除法操作:

int a = 5;
auto value = a/1.0f;

上述表达式就会发生隐式类型转换,混合类型的a/1.0 中的a会被转换成浮点数参与除法运算,因此value的类型也是float。此外,隐式类型转换还会发生在条件表达式以及初始化/赋值操作过程。

类相关的隐式类型转换

上述所谓的隐式类型转换说明了一般情况下的类型转换规则。关于类类型,我们先举如下一个例子:

class  AB;
AB a = 5;

如何能够使上述代码通过编译?从代码本身来看,我们使用一个int类型的对象为一个类类型赋值,我们知道,如果构造函数没有被explicit修饰,上述隐式类型转换就会发生,也就是说 5 会被隐式转换为 AB,并调用拷贝构造函数完成初始化:

class AB {
public:
    AB(int a= 2){};
};

用户自定义的类的隐式类型转换不在这篇文章的讨论范围,感兴趣的可以自己去查看相关内容,按照effective C++的说法,这种操作不被推荐。

运算符重写下的隐式类型转换

effective C++ 条款 24 的名称:若所有参数都需要隐式类型转换,请为此定义非成员函数。对于运算符来讲,这通常对应双目运算符(原文提到乘法满足交换律,而减法操作不满足交换律但仍然满足这种情况的定义,所以交换律并非必须)。我们这里给出一个比原文更常见的操作:字符串的拼接。
我们知道,’+’法操作可以连接std::string 和const char*,在给出正确的定义之前,我们先猜想一下可能的定义:

首先,成员函数的定义方式不满足要求,因为我们知道 “abc”+s(s为std::string 对象)是可以通过编译的,若为成员函数,s作为隐含的操作对象必须在运算符左侧,也即是 s.operator+(“abc”),显然,对+号操作符的重写不可能是成员函数,这也符合我们上面给出的双目运算符规律。对于非成员函数的定义,按照惯常的理解,我们似乎需要定义两个函数来对应上述两种操作,一种string在左侧,一种string在右侧,大概类似这种情况:

string operator+(const string& s1,const char*s2);
string operator+(const char*s1,const string&s2);

按照条款3,这里的返回值string应该是const,否则一个运算了+法操作返回的临时变量仍然可能被赋值(string s1, s2;(s1 + “abc”) = s2; //合法操作)。然而,标准库中源代码并未定义为const,并不知道这样做的原因,先按照标准库的模板来写。

如果没有隐式类型转换,我们基本上就只能按照上述的方式来满足上述的两种情况,但是在类类型的隐式类型转换帮助下,我们只需要给出一种定义:

class string {
public:
    string(const char*){...};
}; // 模拟的定义,实际肯定不是这样的写法

// 非成员函数
string operator+(const string& s1,const string&s2) {}

无论什么形式的string与const char*的相加,都会通过隐式类型转换转换为两个string的求和,并调用上述的加法运算符。

但是,这种隐式类型转换有一个必须的前提,至少有一个参数是string类型,否则,编译器不会默认进行这种隐式类型转换,因为它认为不存在可以调用的函数。因此,两个const char*是不可能直接相加的(除非自定义相关的重载加法操作)。这也从根本上解释了为什么加法操作必须有一个string的原因。

模板类型的操作符重载

模板的情况稍有不同,主要原因在于,模板不会进行隐式类型转换,只有在模板具现化为真正的类之后,编译器才会进行隐式类型转换,关于模板的很多特殊情况,包括继承情况下模板不会查找基类成员函数,这其实都是跟模板编译期表现的一些性质相关,这一点可以查看effective C++ 模板章节,本节只讨论操作符重载。

本节采用的例子均来自effective C++ 条款46。
我们先给出一个Rational 模板的定义,该模板主要做一些数的相关操作。

template <class T>
class Rational {
    public:
    Rational(const T&  numerator = 0, const T& denominator = 1);
    const T numerator() const;
    const T denominator() const;
};

按照与非模板类似的写法,我们可能写出如下的重载*运算符:

template <class T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);

但上述操作不支持混合运算,如下的操作就不会通过编译:

Rational<int> a;
a*2;

编译器不会对模板进行隐式的类型转换,在类型推导过程中,因为operator*函数并未被具现化,2并不会被隐式转换为Rational,即使它确实可以被转换为上述类型。所以不存在这样的混合操作。

解决上述问题的方法就是,找到一种策略,使得重载的运算符在类型定义时就被具现化,从而使隐式类型转换发生作用,一个显而易见的解法就是,将该重载操作定义为类的友元函数:

template <class T>
class Rational {
    public:
    Rational(const T&  numerator = 0, const T& denominator = 1);
    const T numerator() const;
    const T denominator() const;
    friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};

我们已经在类内声明了友元函数,惯常的做法是在类外提供该友元函数的定义,但事实上,这对模板类行不通:具现化的友元函数找不到类外的正确定义。正确的做法是,在模板类内部提供友元函数的定义!事实上,这也是在类内定义非成员函数的做法。

// 以下代码在模板类内
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator()*rhs.numerator(),lhs....);
}

额外的补充

在本节的末尾,书中提到,因为所有在类内定义的函数都自动加上inline,为了抵消inline可能带来的代码膨胀,通常会在类外定义一个辅助函数,而重载的运算符只是简单的调用这个辅助函数(关于为什么会造成代码膨胀,请查看对应章节),事实上,这也是标准库的做法:

// 先给出Rational的声明式:
template<class T>
class Rational;

template<class T>

const Rational<T> doMulti(const Rational<T> &lhs, const Rational<T> &rhs) 
{
    // 做真正的操作
    return Rational(lhs.numerator()*rhs.numerator(),lhs....);
}

// 以下代码在模板类内
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return doMulti(lhs,rhs);
}

模板的定义通常也放在头文件内(所以要同时包含声明和定义)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值