您的位置:首页 > 其它

隐式转换

2014-05-09 23:48 190 查看
隐式转换

转载自http://sunboy1324.blog.163.com/blog/static/134037820071112101815864/ ,谢谢分享。

C++虽然是强类型语言,但是却还不如Java、C#那么足够的强类型,原因是允许的隐式转换太多

从C语言继承下来的基本类型之间的隐式转换
T*指针到void*的隐式转换
non-explicit constructor 接受一个参数的隐式转换
从子类到基类的隐式转换(安全)
从const到non-const的同类型的隐式转换(安全)

除开上面的五种隐式转换外,C++的编译器还非常聪明,当没法直接隐式转换的时候,它会尝试间接的方式隐式转换,这使得有时候的隐式转换非常的微妙,一个误用会被编译器接受而会出现意想不到的结果。例如假设类A有一个non-explicit constructor,唯一的参数是类B,而类B也有一个non-explicitconstructor接受类型C,那么当试图用类型C的实例初始化类A的时候,编译器发现没有直接从类型C构造的过程,但是呢,由于类B可以被接受,而类型C又可以向类型B隐式转换,因此从C->B->A的路就通畅了。这样的隐式转换多数时候没什么大碍,但是不是我们想要的,因为它可能造成一些微妙的bug而难以捕捉。

为了在培训的时候展示栈上析构函数的特点和自动资源管理,准备下面的一个例子,结果测试的时候由于误用而发现一些问题。(测试的IDE是Visual Studio 2005)

class A

{

public:

A(){ a = 100; }

int a;

void f();

};

A * pa = new A();

std::auto_ptr<A> p = pa; //无意这样使用的,本意是std::auto_ptr<A> p(pa)

p->f();

这个写法是拷贝构造函数的形式,显然从T*是不能直接拷贝构造的auto_ptr的,但是编译器会尝试其他的路径来转换成auto_ptr来拷贝构造,因此如果存在一个中间的,这个类能接受从T*的构造,而同时auto_ptr也能接受从类X的构造,那编译器就会很高兴的生成这样的代码。

这段代码在VC6上是通不过的,因为VC6的auto_ptr实现就只有一个接受T*指针的explicit constructor.

但是C++ Standard的修正规范中,要求auto_ptr还应该有个接受auto_ptr_ref的constructor。那么这个auto_ptr_ref是什么呢?按照C++ Standard的解释:

Template auto_ptr_ref holds a reference toan auto_ptr. It is used by the auto_ptrconversions to allow auto_ptrobjects tobe passed to and returned from functions.

有兴趣可以参考Scott Meyers 的 " auto_ptr update page " (http://www.awprofessional.com/content/images/020163371X/autoptrupdate%5Cauto_ptr_update.html )讲诉auto_ptr的历史.

再回到前面的代码,本来应该是通不过的编译,但是VC2005的编译器却没有任何怨言的通过(即使把警告等级设置到4)。结果运行的时候却崩溃了,出错在auto_ptr的析构函数,delete的指针所指向地址是100,而如果在p->f()后面加上一句 cout << pa->a << endl; 发现输出结果为0。为什么会这样,原因就是前面所诉的间接的隐式转换,这与VC 2006的auto_ptr和auto_ptr_ref实现有关,看看P.J.Plauger是怎么实现的:

// auto_ptr_ref

template<class _Ty>

struct auto_ptr_ref

{

// proxy reference for auto_ptr copying

auto_ptr_ref(void *_Right)

: _Ref(_Right)

{ // construct from genericpointer to auto_ptr ptr

}

void *_Ref;// generic pointer to auto_ptrptr

};

// construct auto_ptr from an auto_ptr_refobject

auto_ptr(auto_ptr_ref<_Ty> _Right)_THROW0()

{

// construct by assuming pointer from_Right auto_ptr_ref

_Ty **_Pptr = (_Ty **)_Right._Ref;

_Ty *_Ptr = *_Pptr;

*_Pptr = 0;

// release old

_Myptr = _Ptr;

// reset this

}

这样代码通过编译的原因也就清楚了,A* -> void * -> auto_ptr_ref -> auto_ptr -> copyconstructor -> accept. 好长的隐式转换链, -_-, C++编译器太聪明了。

那么为什么最后会出现指针被破坏的结果呢,原因在auto_ptr的实现,因为按照C++ Standard要求,auto_ptr_ref应该是包含一个auto_ptr的引用,因此auto_ptr的构造函数也就假设了auto_ptr_ref的成员_Ref是一个指向auto_ptr的指针。而auto_ptr中只有一个成员就是A*的指针,因此指向auto_ptr对象的指针相当于就是个A**指针,因此上面auto_ptr从auto_ptr_ref构造的代码是合理的。但是由于罪恶的void*造成了一条非常宽敞的隐式转换的道路,A*指针也能够被接受,因此把A*当作A**来使用,结果可想而知,
A*指向地址的前4个字节(因为32位OS)被拷贝出来,而这四个字节被赋值为0( *_Pptr=0 )。所以出现了最后的结果是_Myptr 值为100,而pa->a为0。

如果要正确执行结果,只要保证是个A**指针就行了,有两个方法

第一,auto_ptr_ref所包含的引用是指向的auto_ptr对象

A * p = new A();

std::auto_ptr<A> pt( new A() );

std::auto_ptr_ref<A> ra( pt );

std::auto_ptr<A> pb = ra;

pb->f();

第二,直接用二级指针

A * p = new A();

std::auto_ptr<A> pb = &p; //这句话后, p将等于0

pb->f();

当然第二种是利用了VC2005的实现而造出来的,看着很别扭,:)。我不明白P.J.Plauger为什么用void *,而不是用auto_ptr<T>&,因为任何指针都能隐式转换为void *,这样的危险性大多了。并且如果用了auto_ptr<T>&,从auto_ptr_ref构造也容易写得更简单清楚,看看以前的实现方式吧,仍然是P.J.Plauger的,但是版本低了点:

template<class _Ty>

struct auto_ptr_ref

{

// proxy reference for auto_ptr copying

auto_ptr_ref(auto_ptr<_Ty>&_Right)

: _Ref(_Right)

{

// construct from compatible auto_ptr

}

auto_ptr<_Ty>& _Ref;

// reference to constructor argument

};

auto_ptr(auto_ptr_ref<_Ty> _Right)_THROW0()

: _Myptr(_Right._Ref.release())

{

// construct by assuming pointer from_Right auto_ptr_ref

}

这样的实现方法,显然不能接受任何指针的隐式转换,也就防止一开始的那种错误写法,并且也是符合C++ Standard的要求的。

而SGI STL的auto_ptr_ref的实现则是包含了一个T*的指针,构造auto_ptr时候直接从auto_ptr_ref中拷贝这个指针,因此这样的实现可以上代码编译通过,运行也正确,不过不符合C++ Standard。

总结一下,危险的潜伏bug的隐式转换应该被杜绝的,特别是void *的隐式转换和构造函数的隐式转换,因此建议是:

慎用void *,因为void *必须要求你知道转换前的实现,因此更适合用在底层的、性能相关的内部实现。
单一参数的构造函数应该注意是否允许隐式转换,如果不需要,加上explicit。例如STL容器中vector接受从int的构造函数,用于预先申请空间,这样的构造函数显然不需要隐式转换,因此加上了explicit。

重载函数中,如果可能,就用更有明确意义的名字替代重载,因为隐式转换也许会带来一些意想不到的麻烦。
避免隐式转换不等于是多用显示转换。Meyers在Effective C++中提到,即使C++风格的显示转换也应该尽量少用,最好是改进设计。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: