STLport源代码中的一个BUG

来源:互联网 发布:c语言算法经典书籍 编辑:程序博客网 时间:2024/04/29 06:50

STLport是世界上使用最广泛的开源STL实现,很多人通过学习STLport源代码来了解STL中的实现细节。

STLport中的copy算法用于将一个容器中指定范围的元素拷贝到另一个容器中。它是这么实现的(原始代码中有很多编译宏隔离,为了表述方便,只展开预编译选项中有效的代码,下同):

// _algobase.htemplate <class _InputIter, class _OutputIter>inline _OutputIter copy(_InputIter __first, _InputIter __last, _OutputIter __result) {  _STLP_DEBUG_CHECK(_STLP_PRIV __check_range(__first, __last))  return _STLP_PRIV __copy_aux(__first, __last, __result, _BothPtrType< _InputIter, _OutputIter>::_Answer());}

这里是想在编译期根据迭代器的不同类型,选择调用不同的函数。_BothPtrType模板判断copy的参数是否都是指针类型,如果是,就会调用这个函数版本:

// _algobase.htemplate <class _InputIter, class _OutputIter>inline _OutputIter __copy_aux(_InputIter __first, _InputIter __last, _OutputIter __result,                              const __true_type& /*BothPtrType*/) {  return _STLP_PRIV __copy_ptrs(__first, __last, __result,                                _UseTrivialCopy(_STLP_VALUE_TYPE(__first, _InputIter),                                                _STLP_VALUE_TYPE(__result, _OutputIter))._Answer());}

这里又做了一次分发,依据的是_UseTrivialCopy模板。如果模板返回__true_type,就会调用下面这个函数:

// _algobase.htemplate <class _InputIter, class _OutputIter>inline _OutputIter __copy_ptrs(_InputIter __first, _InputIter __last, _OutputIter __result,                               const __true_type& /*IsOKToMemCpy*/) {  return (_OutputIter)_STLP_PRIV __copy_trivial(__first, __last, __result);}inline void* __copy_trivial(const void* __first, const void* __last, void* __result) {  size_t __n = (const char*)__last - (const char*)__first;  return __n ? (void *)((char*)memmove(__result, __first, __n) + __n) : __result;}

省事了,直接用memmove代替for循环来完成元素拷贝。这是STL的一大优点:根据参数的类型选择性能最优的操作方式

那么,问题的关键在于怎样判断参数类型是可以直接用memmove拷贝的?首先,两个参数必须都是原生指针,这在第一步由_BothPtrType模板判断。接下来,对于这两个指针,用_UseTrivialCopy模板判断能否直接memmove。下面来看看_UseTrivialCopy模板的实现:

// type_traits.htemplate <class _Src, class _Dst>inline _TrivialCopy<_Src, _Dst> _UseTrivialCopy(_Src*, _Dst*){ return _TrivialCopy<_Src, _Dst>(); }template <class _Src, class _Dst>struct _TrivialCopy {  typedef typename _TrivialNativeTypeCopy<_Src, _Dst>::_Ret _NativeRet;    // 是否内置基本类型  typedef typename __type_traits<_Src>::has_trivial_assignment_operator _Tr1;    // 是否需要调用operator=赋值操作符重载  typedef typename _AreCopyable<_Src, _Dst>::_Ret _Tr2;    // 两个类型间是否允许拷贝  typedef typename _Land2<_Tr1, _Tr2>::_Ret _UserRet;    // 综合判断,“与”  typedef typename _Lor2<_NativeRet, _UserRet>::_Ret _Ret;    // 综合判断,“或”  static _Ret _Answer() { return _Ret(); }};

这个模板的判断分了几个步骤:
1、对于元素为charint,指针等等的内置类型,用_TrivialNativeTypeCopy模板判断能否采用memmove
2、对于用户自定义的类,结构体等类型,则综合看是否有operator=和是否允许拷贝。

再详细看看第一种情况:内置基本类型。_TrivialNativeTypeCopy模板的实现是这样的:

// type_traits.htemplate <class _Src, class _Dst>struct _TrivialNativeTypeCopy {  typedef typename _IsPtr<_Src>::_Ret _Ptr1;    // Src是否是指针  typedef typename _IsPtr<_Dst>::_Ret _Ptr2;    // Dst是否是指针  typedef typename _Land2<_Ptr1, _Ptr2>::_Ret _BothPtrs;    // 是否Src和Dst都是指针  typedef typename _IsCVConvertibleIf<_BothPtrs, _Src, _Dst>::_Ret _Convertible;    // 两个指针能否自动转换  typedef typename _Land2<_BothPtrs, _Convertible>::_Ret _Trivial1;    // 都满足则可以memmove  typedef typename __bool2type<(sizeof(_Src) == sizeof(_Dst))>::_Ret _SameSize;    // 元素大小是否相等  typedef typename _IsIntegral<_Src>::_Ret _Int1;    // Src是否是整型  typedef typename _IsIntegral<_Dst>::_Ret _Int2;    // Dst是否是整型  typedef typename _Land2<_Int1, _Int2>::_Ret _BothInts;    // 是否Src和Dst都是整型  typedef typename _IsRational<_Src>::_Ret _Rat1;    // Src是否是有理数(浮点型)  typedef typename _IsRational<_Dst>::_Ret _Rat2;    // Dst是否是有理数(浮点型)  typedef typename _Land2<_Rat1, _Rat2>::_Ret _BothRats;    // 是否Src和Dst都是有理数(浮点型)  typedef typename _Lor2<_BothInts, _BothRats>::_Ret _BothNatives;    // 都是整型或者都是有理数  typedef typename _Land2<_BothNatives, _SameSize>::_Ret _Trivial2;    // 并且具有同样的大小  typedef typename _Lor2<_Trivial1, _Trivial2>::_Ret _Ret;};

对于判断步骤的说明我写在了注释中。显然,对于元素类型都是整型或者浮点型的情况,用memmove操作是没有疑问的。而对于元素类型是指针的情形,另外又调用了一个模板_IsCVConvertibleIf来判断能否直接memmove拷贝:

// type_traits.htemplate <class _ArePtrs, class _Src, class _Dst>struct _IsCVConvertibleIf{ typedef typename _IsCVConvertible<_Src, _Dst>::_Ret _Ret; };// type_manips.htemplate <class _Src, class _Dst>struct _IsCVConvertible {  typedef _ConversionHelper<_Src, _Dst> _H;  enum { value = (sizeof(char) == sizeof(_H::_Test(false, _H::_MakeSource()))) };  typedef typename __bool2type<value>::_Ret _Ret;};template <class _Src, class _Dst>struct _ConversionHelper {  static char _Test(bool, _Dst);  static char* _Test(bool, ...);  static _Src _MakeSource();};

这个实现可就相当的tricky了。代码中定义了两个同名的_Test函数,但参数类型不同。在传入不同的参数时,编译器会根据参数类型选择用哪一个函数版本。如果_Src可以自动类型转换为_Dst,那么编译器就会匹配到char _Test(bool, _Dst)这个函数上去,返回值就是char。因此可以根据函数调用的返回值为char来推断出_Src可以自动类型转换为_Dst。真是高明!可问题是:只要源指针能自动转换为目标指针,就可以用memmove来拷贝指针吗?

我在《万恶的void*指针类型转换》这篇博客中,曾经指出C++中由于多重继承的原因,在指针类型转换时,指针的值也会跟着变化。在那篇文章里我详细分析了指针值会变化的原因,并警示了当有类型转换时,绝不能暴力的使用原始指针值。

那么显然,当_Src指针转换为_Dst指针时,其值可能会变,用memmove来拷贝可能出现错误!

是否真的如此?我写了一个小程序来测试一下:

#include <vector>#include <algorithm>#include <iostream>using namespace std;class BaseA{public:    int elementA;    virtual void funcA()    {        cout << "I am funcA!" << endl;    };};class BaseB{public:    int elementB;    virtual void funcB()    {        cout << "I am funcB!" << endl;    };};class Derived : public BaseA, public BaseB{public:    int elementD;    virtual void funcA()    {        cout << "I am derived for funcA!" << endl;    };    virtual void funcB()    {        cout << "I am derived for funcB!" << endl;    };};int _tmain(int argc, char* argv[]){    vector<Derived*> v_derived;    vector<BaseB*> v_baseb(1);    // 容器大小为1    Derived* derived = new Derived();    v_derived.push_back(derived);    copy(v_derived.begin(), v_derived.end(), v_baseb.begin());    // 好戏开始了!    BaseB* baseb = derived;    // 这个是用来演示正确的赋值结果,以作对比。    cout << "derived pointer : " << v_derived.front() << endl         << "copied base pointer : " << v_baseb.front() << endl         << "cast base pointer : " << baseb << endl;    baseb = v_baseb.front();    baseb->funcB();    return 0;}

使用STLport的头文件,程序在VS2008下编译通过,运行结果为:

derived pointer : 0x00112928
copyed base pointer : 0x00112928
cast base pointer : 0x00112930
I am derived for funcA!

Aha!末日来了!正常赋值得到的指针和用copy算法得到的指针值不一样!如果你用这个错误的值去调用虚函数,会调用到错误的函数上;如果用这个指针去访问成员,就会出现严重的运行时错误。当你碰到这个诡异的运行时错误时,会想得到这是STLport源代码的BUG吗?

STLport外,其他版本的STL实现是否有问题呢?我迫不及待的尝试了下VS2008自带的STL,同样的程序,运行结果如下:

derived pointer : 001F12D8
copyed base pointer : 001F12E0
cast base pointer : 001F12E0
I am derived for funcB!

谢天谢地!这回终于运行正确了。看来VS自带的STL版本并没有这种BUG。看看它的源代码是怎么实现的吧:

// xutilitytemplate<class _InIt, class _OutIt>inline_IF_CHK(_OutIt) __CLRCALL_OR_CDECL copy(_InIt _First, _InIt _Last, _OutIt _Dest)    {   // copy [_First, _Last) to [_Dest, ...)    return (_Copy_opt(_CHECKED_BASE(_First), _CHECKED_BASE(_Last), _Dest,        _Iter_random(_First, _Dest), _Ptr_cat(_First, _Dest), _Range_checked_iterator_tag()));    }/* use _Ptr_cat_helper to determine the type of the pointer category */template<class _T1, class _T2> inlinetypename _Ptr_cat_helper<_T1, _T2>::_Ptr_cat __CLRCALL_OR_CDECL _Ptr_cat(_T1&, _T2&)    {    typename _Ptr_cat_helper<_T1, _T2>::_Ptr_cat _Cat;    return (_Cat);    }template<class _Ty>struct _Ptr_cat_helper<_Ty **, _Ty **>    {   // return pointer category from pointer to pointer arguments    typedef _Scalar_ptr_iterator_tag _Ptr_cat;    // 可以memmove    };template<class _T1,  class _T2>struct _Ptr_cat_helper    {    typedef typename _Ptr_cat_with_checked_cat_helper<_T1, _T2,        typename _Checked_iterator_category<_T1>::_Checked_cat,        typename _Checked_iterator_category<_T2>::_Checked_cat>::_Ptr_cat _Ptr_cat;    };template<class _T1, class _T2, class _Checked_Cat1, class _Checked_Cat2>struct _Ptr_cat_with_checked_cat_helper    {    typedef _Nonscalar_ptr_iterator_tag _Ptr_cat;    // 不可以memmove    };template<class _InIt, class _OutIt, class _InOutItCat>inline    _OutIt __CLRCALL_OR_CDECL _Copy_opt(_InIt _First, _InIt _Last, _OutIt _Dest,        _InOutItCat, _Nonscalar_ptr_iterator_tag, _Range_checked_iterator_tag)    {   // copy [_First, _Last) to [_Dest, ...), arbitrary iterators    _DEBUG_RANGE(_First, _Last);    for (; _First != _Last; ++_Dest, ++_First)        *_Dest = *_First;    return (_Dest);    }template<class _InIt, class _OutIt, class _InOutItCat>inline    _OutIt __CLRCALL_OR_CDECL _Copy_opt(_InIt _First, _InIt _Last, _OutIt _Dest,        _InOutItCat, _Scalar_ptr_iterator_tag, _Range_checked_iterator_tag)    {   // copy [_First, _Last) to [_Dest, ...), pointers to scalars    ptrdiff_t _Off = _Last - _First;    // NB: non-overlapping move    // if _OutIt is range checked, this will make sure there is enough space for the memmove    _OutIt _Result = _Dest + _Off;    if (_Off > 0)        _CRT_SECURE_MEMMOVE(&*_Dest, _Off * sizeof (*_First), &*_First, _Off * sizeof(*_First));    return _Result;    }

看得出来,VS的源代码对于指针元素是否采用memmove的判断标准是“两个指针类型是否相同”,而不是“两个指针能否自动转换”。因此保证了功能的正确性。

经检查,STLport中除了copy算法外,uninitialized_copy算法也有同样的问题。这个BUG即使在最新的STLport 5.2.1版本中也存在,一旦被撞上,后果将是奇怪的运行时错误。作为一套全世界广泛使用的开源代码,存在这样的问题实在不太应该。

对于STLport的用户来说,我的建议是尽量避免在存在类型转换的指针之间进行操作,至少避免对于有多重继承的指针进行操作。最后吐槽一句:C++的多重继承真是个大陷阱。

0 0