[轉(zhuǎn)]智能指針與微妙的隱式轉(zhuǎn)換
??? C++
雖然是強類型語言,但是卻還不如
Java
、
C#
那么足夠的強類型,原因是允許的隱式轉(zhuǎn)換太多
-
從
C
語言繼承下來的基本類型之間的隱式轉(zhuǎn)換
-
T*
指針到
void*
的隱式轉(zhuǎn)換
-
non-explicit constructor
接受一個參數(shù)的隱式轉(zhuǎn)換
-
從子類到基類的隱式轉(zhuǎn)換
(
安全)
-
從
const
到
non-const
的同類型的隱式轉(zhuǎn)換
(
安全
)
除開上面的五種隱式轉(zhuǎn)換外,
C++
的編譯器還非常聰明,當(dāng)沒法直接隱式轉(zhuǎn)換的時候,它會嘗試間接的方式隱式轉(zhuǎn)換,這使得有時候的隱式轉(zhuǎn)換非常的微妙,一個誤用會被編譯器接受而會出現(xiàn)意想不到的結(jié)果。例如假設(shè)類
A
有一個
non-explicit constructor
,唯一的參數(shù)是類
B
,而類
B
也有一個
non-explicit constructor
接受類型
C
,那么當(dāng)試圖用類型
C
的實例初始化類
A
的時候,編譯器發(fā)現(xiàn)沒有直接從類型
C
構(gòu)造的過程,但是呢,由于類
B
可以被接受,而類型
C
又可以向類型
B
隱式轉(zhuǎn)換,因此從
C->B->A
的路就通暢了。這樣的隱式轉(zhuǎn)換多數(shù)時候沒什么大礙,但是不是我們想要的,因為它可能造成一些微妙的
bug
而難以捕捉。
?
為了在培訓(xùn)的時候展示棧上析構(gòu)函數(shù)的特點和自動資源管理,準(zhǔn)備下面的一個例子,結(jié)果測試的時候由于誤用而發(fā)現(xiàn)一些問題。
(
測試的
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();
?
這個寫法是拷貝構(gòu)造函數(shù)的形式,顯然從
T*
是不能直接拷貝構(gòu)造的
auto_ptr
的,但是編譯器會嘗試其他的路徑來轉(zhuǎn)換成
auto_ptr
來拷貝構(gòu)造,因此如果存在一個中間的
,這個類能接受從
T*
的構(gòu)造,而
同時auto_ptr也能接受從類X
的構(gòu)造,那編譯器就會很高興的生成這樣的代碼。
這段代碼在
VC6
上是通不過的,因為
VC6
的
auto_ptr
實現(xiàn)就只有一個接受
T*
指針的
explicit constructor
.
但是
C++ Standard
的修正規(guī)范中,要求
auto_ptr
還應(yīng)該有個接受
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的歷史.
?
再回到前面的代碼,本來應(yīng)該是通不過的編譯,但是
VC2005
的編譯器卻沒有任何怨言的通過
(
即使把警告等級設(shè)置到
4)
。結(jié)果運行的時候卻崩潰了,出錯在
auto_ptr
的析構(gòu)函數(shù)
,delete
的指針?biāo)赶虻刂肥?/span>
100
,而如果在
p->f()
后面加上一句
cout << pa->a << endl;
發(fā)現(xiàn)輸出結(jié)果為
0
。
為什么會這樣,原因就是前面所訴的間接的隱式轉(zhuǎn)換,這與
VC 2006
的
auto_ptr
和
auto_ptr_ref
實現(xiàn)有關(guān),看看
P.J.Plauger
是怎么實現(xiàn)的
:
// 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.
好長的隱式轉(zhuǎn)換鏈
, -_-, C++
編譯器太聰明了。
那么為什么最后會出現(xiàn)指針被破壞的結(jié)果呢,原因在
auto_ptr
的實現(xiàn),因為按照
C++ Standard
要求,
auto_ptr_ref
應(yīng)該是包含一個
auto_ptr
的引用,因此
auto_ptr
的構(gòu)造函數(shù)也就假設(shè)了
auto_ptr_ref
的成員
_Ref
是一個指向
auto_ptr
的指針。
而
auto_ptr
中只有一個成員就是
A*
的指針,因此指向
auto_ptr
對象的指針相當(dāng)于就是個
A**
指針,因此上面
auto_ptr
從
auto_ptr_ref
構(gòu)造的代碼是合理的。
但是由于罪惡的
void*
造成了一條非常寬敞的隱式轉(zhuǎn)換的道路,
A*
指針也能夠被接受,因此把
A*
當(dāng)作
A**
來使用,結(jié)果可想而知,
A*
指向地址的前
4
個字節(jié)
(
因為
32
位
OS)
被拷貝出來,而這四個字節(jié)被賦值為
0( *_Pptr=0 )
。
所以出現(xiàn)了最后的結(jié)果是
_Myptr
值為
100
,而
pa->a
為
0
。
如果要正確執(zhí)行結(jié)果,只要保證是個
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();
?
當(dāng)然第二種是利用了
VC2005
的實現(xiàn)而造出來的,看著很別扭
,:)
。
我不明白
P.J.Plauger
為什么用
void *
,而不是用
auto_ptr<T>&
,因為任何指針都能隱式轉(zhuǎn)換為
void *
,這樣的危險性大多了。并且如果用了
auto_ptr<T>&
,從
auto_ptr_ref
構(gòu)造也容易寫得更簡單清楚,看看以前的實現(xiàn)方式吧,仍然是
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
}
?
這樣的實現(xiàn)方法,顯然不能接受任何指針的隱式轉(zhuǎn)換,也就防止一開始的那種錯誤寫法,并且也是符合
C++ Standard
的要求的。
而
SGI STL
的
auto_ptr_ref
的實現(xiàn)則是包含了一個
T*
的指針,構(gòu)造
auto_ptr
時候直接從
auto_ptr_ref
中拷貝這個指針,因此這樣的實現(xiàn)可以上代碼編譯通過,運行也正確,不過不符合
C++ Standard
。
?
總結(jié)一下,危險的潛伏bug的隱式轉(zhuǎn)換應(yīng)該被杜絕的,特別是
void *
的隱式轉(zhuǎn)換和構(gòu)造函數(shù)的隱式轉(zhuǎn)換,因此建議是
:
-
慎用
void *
,因為
void *
必須要求你知道轉(zhuǎn)換前的實現(xiàn),因此更適合用在底層的、性能相關(guān)的內(nèi)部實現(xiàn)。
-
單一參數(shù)的構(gòu)造函數(shù)應(yīng)該注意是否允許隱式轉(zhuǎn)換,如果不需要,加上
explicit
。例如
STL
容器中
vector
接受從
int
的構(gòu)造函數(shù),用于預(yù)先申請空間,這樣的構(gòu)造函數(shù)顯然不需要隱式轉(zhuǎn)換,因此加上了
explicit
。
-
重載函數(shù)中,如果可能,就用更有明確意義的名字替代重載,因為隱式轉(zhuǎn)換也許會帶來一些意想不到的麻煩。
-
避免隱式轉(zhuǎn)換不等于是多用顯示轉(zhuǎn)換。
Meyers
在
Effective C++
中提到,即使
C++
風(fēng)格的顯示轉(zhuǎn)換也應(yīng)該盡量少用,最好是改進設(shè)計。
posted on 2006-07-25 00:37
Jerry Cat 閱讀(769)
評論(0) 編輯 收藏 引用