《C++编程惯用法——高级程序员常用方法和技巧》——2.4 隐式类型转换

简介:

本节书摘来自异步社区出版社《C++编程惯用法——高级程序员常用方法和技巧》一书中的第2章,第2.4节,作者: 【美】Robert B. Murray ,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.4 隐式类型转换

隐式类型转换(详情参见下面的回顾)会使得编译器能够在暗中修改我们所写的代码。由于这个原因,我们必须谨慎地提供和使用它们:人们很难理解那些有着大量隐式类型转换的代码。而且,使用这种转换的场合越多,我们就越可能得到有着歧义的函数调用,因此也就需要更多的显式转换来消除这种歧义。在本节中,我们将向读者展示一些经常发现在使用隐式类型转换的代码中的问题。我们同样也会向读者给出一些关于“该在何时使用隐式类型转换”的建议。

回顾:隐式类型转换

我们可以通过如下两种方式来声明一个从类型F到类型T的隐式转换:

在T中声明一个仅接受一个类型为F的参数的构造函数(这其中也包括那些使用了缺省参数值的构造函数,如:T::T(F,int=0))
在F中声明一个operator T的转换函数
如果在函数调用中无法找到形式参数与实际参数类型完全匹配的函数声明,编译器就将试图通过在实参上进行隐式转换来找到合适的函数。对于作用域中所有同名的函数,编译器会试着将用户自定义的(至多一个)隐式转换作用到每个实参之上来寻找匹配程度最高的函数声明。如果编译器找到这样的一个函数,那么它就会在目标代码中使用这个转换,最后调用该函数;否则,编译器就会向外报告一个编译错误。

class Rational {
public:
//…
     Rational(int,int);
     oporator double();
);
double sqrt(double);
 
main() {
     Rational r(4,1);
     double sq = sqrt(r);
};

在上面的代码中,r就被隐式地转换为一个double值,就好比如下的用户代码:

double dval = r.operator double();
double sq = sqrt(dval);

2.4.1 带有单个参数的构造函数

带有单个参数的构造函数同时也是一个隐式转换,这个事实让人觉得很不方便,因为我们希望的是该构造函数应该用于明确的对象创建过程中。只有在概念上将同样的信息由一种格式转换成另外一种格式的前提下,我们才允许有这样的隐式转换。如果在概念上,被转换后的对象与构造函数的参数不同,这就很让人费解。我们将通过一些例子来向大家展示这一灰色区域:

class String {
//此处忽略细节
public:
   String(const char* = "");
};

void print_heading(const String&);
//…
print_heading( "Annual Report");

在调用print_heading时,我们就会得到一个隐式的从char到String的转换;由于String和char在概念上来说是同一抽象模型的两个不同表示方法,所以这样的操作是合理的。

class Rational {
//此处忽略细节
public:
   Rational(long num=0, long denom = 1);
);

int operator==(const Rational&, const Rational&);

int 
nonzero(const Rational& r){
   return r == 0;
}

上面这个例子虽然不是很清晰,但同样也可以让人明了。0在这被隐式转换成一个Rational(在Rational的构造函数中,第二个参数有着一个缺省的值1)。然而,在这样写的代码中,那个比较操作看起来要比它实际上的操作要显得更高效一些。它的显式版本如下:

int 
zero(const Rational& r){
   return r == Rational(0,1);
}

上面的代码就可以给我们一个更好的关于该函数的实际性能如何的描述,并且它还可以向我们暗示出如下的优化:

int
zero (const Rational& r){
   static const Rational zero(0,1);
   return r == zero;
}

这使得我们可以节省下每次都创建和摧毁一个新的Rational对象zero所产生的开销。只有那些精明的程序员才可以从函数的最初版本中发现这种优化。

下面是另外一个例子:

#include <stdlib.h>
//随机数产生器:
class Random_gen {
//此处忽略细节
public:
  Random_gen(long seed);
};

void play_game(Random_gen);
main(int argc, char* argv[]){
if (argc>1)
   play_game(atoi(argv[1])); //让人迷惑的地方
}

上面的程序中本来是不应该出现隐式转换的。一个用于产生随机数的对象和它用来产生随机数的种子(seed)应该分属于不同的对象;尤其是当它使用了一个long作为其构造函数参数时,它的类型就和这个种子完全不一样了。在上面的代码中,“传递给play_game的参数并不是atoi函数的输出”这个事实很容易被人所忽略。这段代码应该使用显式调用构造函数的方式来重写:

int 
main(int argc, char*argv[]){
if (argc>1){
   Random_gen gen(atoi(argv[1]));
   play_game(gen);//现在好多了
}
//…
}

上面的代码和最初的代码在执行时是一样快的(不管我们有没有写构造函数的调用代码,编译器都会为我们产生它),但它更能体现程序的意图。

2.4.2 类型转换操作符

有着多个类型转换操作符的类在被使用时更容易产生编译时期的二义性问题。例如,有一个只带有一个类型转换操作符的类如下:

class String {
   char* rep;
public:
   String(const char*="");
   operator const char*() const { return rep; }
   //此处忽略细节
};

main() {
   String s("hello world");
   cout << s << endl;
}

上面的代码可以正常工作;由于我们没有声明String的输出操作符,代码中的String对象就被转换成一个const char*用于输出。

如果我们往String中再添加一个类型转换操作符,会有什么现象出现呢:

class String {
   char* rep;
public:
   String(const char* = "");
   operator const char*()const{return rep;}
   operator int()const { return atoi(rep);}  //新增的转换操作符
   //此处忽略细节
};

看上去这个新增的转换操作符会给我们带来很多的便利,因为它可以将String用于那些需要int的场合中;但是它却会破坏我们已有的代码:

main() {
   String s("hello world");
   cout << s << endl;//编译期错误:
             //无法确定进行哪种转换
}

现在对于String的输出,编译器的解析步骤将变成在“将String转换成const char*再输出”和“将String转换为int再输出”这两种行为中挑选一种,因此我们就得到了一个编译错误。为了让上述代码顺利通过编译,我们不得不使用显式转换:

cout<< (const char*) s;

在String中同时提供两种隐式转换使得程序中出现歧义的可能性大为增加。

不过这并不意味着我们不能将String转换成为int,我们所要做的只不过是不再使用隐式的方式进行这种转换而已:

class String{
   char* rep;
public:
   int as_int () { return atoi(rep);}
   //此处忽略细节
};

现在,如果希望进行从String到int的转换,我们就需要明确地调用as_int函数:

void 
process_key_value(const String& key,const String& val) {
   if (numeric_key(key)){
     int value = val.as_int ();
     //…

这并不是个坏消息,使用明确的函数调用会使得程序更容易被人所理解。

2.4.3 内建类型之间的隐式转换

对于涉及自定义类型的隐式转换,它们的规则比较固定也容易让人理解。但对于在内建的整型类型之间的隐式转换,它们的规则就显得令人难以捉摸,并且在语言的发展过程中,这些规则也有着一些变化。例如:

void f(long);
void f(int);
main() {
   f('X');//此处调用的是哪个f?
}
上面的代码中调用的是f(int),但我们再来看如下的代码:

void f{long};
void f(unsigned int);
main() {
   f('x');//此处调用的是哪个f?
}

它会产生一个编译时期的错误(有着歧义的函数调用)。可能你对于语言规则十分了解,知道为什么会有这种现象发生,但那些将来维护这段代码的人可能就不清楚它发生的原因了。我们应该避开语言中的那些含义模糊的特性,如果真的需要对不同的内建整型进行不同的操作,我们最好是为每个整型都提供一个参数类型和它完全匹配的函数:

void output(long);
void output(unsigned long);
void output(int);
void output(unsigned int);
void output(short);
void output(unsigned short);
void output(char);
void output(unsigned char);
void output(signed char);

现在,我们就可以确信下面代码的意思不但明显,而且也不会发生改变:

unsigned short s;
output(s);
相关文章
|
8天前
|
算法 编译器 C语言
探索C++编程的奥秘与魅力
探索C++编程的奥秘与魅力
|
1月前
|
算法 Linux 程序员
嵌入式工程师以及C++程序员到公司就业需要掌握那些技术?
嵌入式工程师以及C++程序员到公司就业需要掌握那些技术?
|
9天前
|
编译器 C语言 C++
C语言,C++编程软件比较(推荐的编程软件)
C语言,C++编程软件比较(推荐的编程软件)
|
8天前
|
并行计算 调度 C++
|
2天前
|
存储 编译器 C语言
【C++】C++中规范[ 类型转换标准 ] 的四种形式
【C++】C++中规范[ 类型转换标准 ] 的四种形式
|
4天前
|
安全 程序员 编译器
【C++类和对象】初始化列表与隐式类型转换
【C++类和对象】初始化列表与隐式类型转换
|
5天前
|
算法 程序员 C语言
C++:深度探索与编程实践
C++:深度探索与编程实践
12 3
|
10天前
|
编译器 程序员 C++
C++从入门到精通:3.1模板编程——提高代码的复用性和灵活性
C++从入门到精通:3.1模板编程——提高代码的复用性和灵活性
|
10天前
|
C++
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
C++从入门到精通:2.1.2函数和类——深入学习面向对象的编程基础
|
10天前
|
安全 编译器 C语言
【C++高阶(九)】C++类型转换以及IO流
【C++高阶(九)】C++类型转换以及IO流