3.4 显式转换操作符

类别:库作者

在C++中,有个非常好也非常坏的特性,就是隐式类型转换。隐式类型转换的“自动性”可以让程序员免于层层构造类型。但也是由于它的自动性,会在一些程序员意想不到的地方出现严重的但不易被发现的错误。我们可以先看看代码清单3-26所示的这个例子。

代码清单3-26


include <iostream>

using namespace std;

struct Rational1{

Rational1(int n=0,int d=1):num(n),den(d){

cout<<func<<"("<<num<<"/"<<den<<")"<<endl;

}

int num;//Numerator(被除数)

int den;//Denominator(除数)

};

struct Rational2{

explicit Rational2(int n=0,int d=1):num(n),den(d){

cout<<func<<"("<<num<<"/"<<den<<")"<<endl;

}

int num;

int den;

};

void Display1(Rational1 ra){

cout<<"Numerator:"<<ra.num<<"Denominator:"<<ra.den<<endl;

}

void Display2(Rational2 ra){

cout<<"Numerator:"<<ra.num<<"Denominator:"<<ra.den<<endl;

}

int main(){

Rational1 r1_1=11;//Rational1(11/1)

Rational1 r1_2(12);//Rational1(12/1)

Rational2 r2_1=21;//无法通过编译

Rational2 r2_2(22);//Rational2(22/1)

Display1(1);//Rational1(1/1)

//Numerator:1 Denominator:1

Display2(2);//无法通过编译

Display2(Rational2(2));//Rational2(2/1)

//Numerator:2 Denominator:1

return 0;

}

//编译选项:g++ -std=c++11 3-4-1.cpp


在代码清单3-26中,声明了两个类型Rational1和Rational2。两者在代码上的区别不大,只不过Rational1的构造函数Rational1(int,int)没有explicit关键字修饰,这意味着该构造函数可以被隐式调用。因此,在定义变量r1_1的时候,字面量11就会成功地构造出Rational1(11,1)这样的变量,Rational2却不能从字面量21中构造,这是因为其构造函数由于使用了关键字explicit修饰,禁止被隐式构造,因此会导致编译失败。相同的情况也出现在函数Display2上,由于字面量2不能隐式地构造出Rational2对象,因此表达式Display2(2)的编译同样无法通过。

这里虽然Display1(1)编译成功,不过如果不是结合了上面Rational1的定义,我们很容易在阅读代码的时候产生误解。按照习惯,程序员会误认为Display1是个打印整型数的函数。因此,使用了explicit这个关键字保证对象的显式构造在一些情况下都是必须的。

不过同样的机制并没有出现在自定义的类型转换符上。这就允许了一个逆向的过程,从自定义类型转向一个已知类型。这样虽然出现问题的几率远小于从已知类型构造自定义类型,不过有的时候,我们确实应该阻止会产生歧义的隐式转换。让我们来看看代码清单3-27所示的例子,该例子来源于C++11提案。

代码清单3-27


include <iostream>

using namespace std;

template<typename T>

class Ptr{

public:

Ptr(T*p):_p(p){}

operator bool()const{

if(_p!=0)

return true;

else

return false;

}

private:

T*_p;

};

int main(){

int a;

Ptr<int> p(&a);

if(p)//自动转换为bool型,没有问题

cout<<"valid pointer."<<endl;//valid pointer.

else

cout<<"invalid pointer."<<endl;

Ptr<double> pd(0);

cout<<p+pd<<endl;//1,相加,语义上没有意义

}

//编译选项:g++3-4-2.cpp


在代码清单3-27中,我们定义了一个指针模板类型Ptr。为了方便判断指针是否有效,我们为指针编写了自定义类型转换到bool类型的函数,这样一来,我们就可以通过if(p)这样的表达式来轻松地判断指针是否有效。不过这样的转换使得Ptr<int>和Ptr<double>两个指针的加法运算获得了语法上的允许。不过明显地,我们无法看出其语义上的意义。

在C++11中,标准将explicit的使用范围扩展到了自定义的类型转换操作符上,以支持所谓的“显式类型转换”。explicit关键字作用于类型转换操作符上,意味着只有在直接构造目标类型或显式类型转换的时候可以使用该类型。我们可以看看代码清单3-28所示的例子。

代码清单3-28


class ConvertTo{};

class Convertable{

public:

explicit operator ConvertTo()const{return ConvertTo();}

};

void Func(ConvertTo ct){}

void test(){

Convertable c;

ConvertTo ct(c);//直接初始化,通过

ConvertTo ct2=c;//拷贝构造初始化,编译失败

ConvertTo ct3=static_cast<ConvertTo>(c);//强制转化,通过

Func(c);//拷贝构造初始化,编译失败

}

//编译选项:g++ -std=c++11 3-4-3.cpp


在代码清单3-28中,我们定义了两个类型ConvertTo和Convertable,Convertable定义了一个显式转换到ConvertTo类型的类型转换符。那么对于main中ConvertTo类型的ct变量而言,由于其直接初始化构造于Convertable变量c,所以可以编译通过。而做强制类型转换的ct3同样通过了编译。而ct2由于需要从c中拷贝构造,因而不能通过编译。此外,我们使用函数Func的时候,传入Convertable的变量c的也会导致参数的拷贝构造,因此也不能通过编译。

如果我们把该方法用于代码清单3-27中,可以发现我们预期的事情就发生了,if(p)可以通过编译,因为可以通过p直接构造出bool类型的变量。而p+pd这样的语句就无法通过编译了,这是由于全局的operator+并不接受bool类型变量为参数,而Convertable也不能直接构造出适用于operator+的int类型的变量造成的(不过读者可以尝试一下使用p&&pd这样的表达式,是能够通过编译的)。这样一来,程序的行为将更加良好。

可以看到,所谓显式类型转换并没完全禁止从源类型到目标类型的转换,不过由于此时拷贝构造和非显式类型转换不被允许,那么我们通常就不能通过赋值表达式或者函数参数的方式来产生这样一个目标类型。通常通过赋值表达式和函数参数进行的转换有可能是程序员的一时疏忽,而并非本意。那么使用了显式类型转换,这样的问题就会暴露出来,这也是我们需要显式转换符的一个重要原因。