[轉]智能指針與微妙的隱式轉換
??? 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 閱讀(769)
評論(0) 編輯 收藏 引用