[转]智能指针与微妙的隐式转换
C++
虽然是强类型语言,但是却还不如
Java
、
C#
那么足够的强类型,原因是允许的隐式转换太多
-
从
C
语言继承下来的基本类型之间的隐式转换
-
T*
指针到
void*
的隐式转换
-
non-explicit constructor
接受一个参数的隐式转换
-
从子类到基类的隐式转换
(
安全)
-
从
const
到
non-const
的同类型的隐式转换
(
安全
)
除开上面的五种隐式转换外,
C++
的编译器还非常聪明,当没法直接隐式转换的时候,它会尝试间接的方式隐式转换,这使得有时候的隐式转换非常的微妙,一个误用会被编译器接受而会出现意想不到的结果。例如假设类
A
有一个
non-explicit constructor
,唯一的参数是类
B
,而类
B
也有一个
non-explicit constructor
接受类型
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 to an auto_ptr. It is used by the auto_ptr
conversions to allow auto_ptr
objects to be 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 generic pointer to auto_ptr ptr
}
void *_Ref;// generic pointer to auto_ptr ptr
};
// construct auto_ptr from an auto_ptr_ref object
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 -> copy constructor -> 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++
风格的显示转换也应该尽量少用,最好是改进设计。
posted on 2006-07-25 00:37
Jerry Cat 阅读(760)
评论(0) 编辑 收藏 引用