asm, c, c++ are my all
-- Core In Computer
posts - 139,  comments - 123,  trackbacks - 0

[转]智能指针与微妙的隐式转换

    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)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理



<2006年7月>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

常用链接

留言簿(7)

随笔档案

最新随笔

搜索

  •  

最新评论

阅读排行榜

评论排行榜