1. 转换函数与explicit关键字
1.1 转换函数
下述代码的第5行operator double()
即是一个转换函数,通过这个函数,编译器可以在需要的情况下,直接将Fraction
类型的对象转换为double
类型。这个函数有两个特点:首先因为转换函数的返回类型已经通过函数名double()
给定,即返回double
类型的结果,所以不需要写返回类型;其次,因为这个转换函数并不会更改对象的数据成员,所以将此函数声明为成const
。
编译器在执行第17
行代码double d = 4+f
时,会有很多的判断。其会考虑代码里有没有定义+
号的运算符重载函数,且此重载函数接收double
与fraction
类型的对象作为第一参数与第二参数。若有此函数,则可以直接使用,下述代码是没有写这个重载函数的。编译器还会考虑是否存在f
的转换函数,将f
转换为double
类型来执行相加操作,因为定义了double()
转换函数,所以此过程可以执行成功,进而完成相加的操作。
class Fraction {
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {}
operator double() const {
return (double) (m_numerator / m_denominator);
}
private:
int m_numerator; //分子
int m_denominator; //分母
};
int main() {
Fraction f(3, 5);
double d = 4 + f; //调用operator double()将f转为0.6,并执行相加
}
下述代码第3
行的构造函数,虽然有两个参数(parameter
),但是其最少只需要接收一个,这种构造函数被称为one argument ctor
。这种构造函数比较页数,因为其存在隐式转换的特性。在代码的第15
行,3
通过调用上述的构造函数可以转换为Fraction
类型的匿名对象Fraction(3,1)
,进而调用重载的+
运算符完成加法操作。
class Fraction {
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {}
Fraction operator+(const Fraction &f);
private:
int m_numerator; //分子
int m_denominator; //
};
int main() {
Fraction f(3, 5);
Fraction d2 = f + 3; //调用隐式转换的构造函数将4转为Fraction(4,1),再调用operator+
}
上述的分数类已经很好的说明了转换函数的用途,在数学中将一个分数直接转为double
类型是一个很自然的事情。
在cpp
的标准库中也有转换函数的应用,如下是标准库中一个特化vector
的模板,当vector
存储bool
类型时,在空间存储上或处理逻辑上和别的元素类型都有可能有区别,所以这里是特化的vector<bool, Alloc>
类型的模板。
这里vector
重载的下标运算符[]
可以得到执行下标的元素,但是注意其返回类型并不是bool
类型,而是被代理成了_bit_reference
类型。但是当用户使用标准库时,其期望的返回类型一定是bool
类型,所以这里对_bit_reference
类即定义了转换函数,用于将此类型的对象转换为bool
类型。
1.2 no-explicit & one-argument 构造函数
隐式转换的功能,有时候会导致代码出现二义性,如下代码的第19行,对于这个加法操作,3
可以通过构造函数隐式转为Fraction
类型完成相加,而f
也可以通过转换函数转为double
类型完成相加,在+
操作的执行过程中会出现二义性。
class Fraction {
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {}
operator double() const {
return double(m_numerator / m_denominator);
}
Fraction operator+(const Fraction &f);
private:
int m_numerator; //分子
int m_denominator; //
};
int main() {
Fraction f(3, 5);
Fraction d2 = f + 3;
}
为了消除这种二义性,即禁止编译器的隐式转换,需要在构造函数前加上explict
关键字,这时3
就不能通过这个构造函数隐式转为double
类型。
除了构造函数外,也可以对转换函数加上explicit
关键字,这时这个类就必须通过显式转换的方式转为指定类型:如下所示,当对转换函数加上explicit
后,第23
行将无法通过编译,而必须通过24、25
行的这种显式转换即可。
这里也定义了bool
类型的转换函数,可以发现27
行是无法通过编译的,而28
行在if
判断内,编译器会认为这是一个显式转换,不会报错。
class Fraction {
public:
explicit Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {}
explicit operator double() const {
return double(m_numerator / m_denominator);
}
explicit operator bool() const {
return m_denominator != 0;
}
Fraction operator+(const Fraction &f);
private:
int m_numerator; //分子
int m_denominator; //
};
int main() {
Fraction f(3, 5);
// cout << (f + 2.0) << endl;
cout << ((double) f + 2.0) << endl;
cout << (static_cast<double >(f) + 2.0) << endl;
// bool j = f;
if (f) {
}
}
1.3 c++11后的explicit
在1.2部分叙述的禁止隐式转换在c++11
之前就已经存在,不过其只针对单一参数的构造函数,这种构造函数也可以被称为no-explicit and one-argument 构造函数。在c++11
后出现了统一初始化,所以explicit
也可以针对多参数的情况:
若不存在第七行的接收initializer_list
的构造函数,则第18
行是无法通过编译的。因为第18
行的统一初始化方式执行时,编译器将{77, 5, 42}
包装成一个initializer_list
,因为不存在能够接受initializer_list
的构造函数,编译器会将这三个元素依次分解,传递给第10
行的构造函数,但是此构造函数禁止隐式转转,所以将无法通过编译。在c++11
之后,增加了统一初始化方式,explicit
也可以作用于多参的情形了。
class P {
public:
P(int a, int b) {
cout << "P(int a,int b)" << endl;
}
P(initializer_list<int>) {
cout << "P(initializer_list<int>)" << endl;
}
explicit P(int a, int b, int c) {
cout << "explicit P(int a,int b,int c)" << endl;
}
};
int main() {
P p(77, 5); //调用P(int a,int b)
P p2{77, 5}; //调用P(initializer_list<int>)
P p3{77, 5, 42}; //调用P(initializer_list<int>)
P p4 = {77, 5, 42}; //调用P(initializer_list<int>)
}