STLport源代码中的一个BUG

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

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

// _algobase.h
template 
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.h
template 
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.h
template 
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.h
template 
inline _TrivialCopy<_Src, _Dst> _UseTrivialCopy(_Src*, _Dst*)
{ return _TrivialCopy<_Src, _Dst>(); }

template 
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.h
template 
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.h
template 
struct _IsCVConvertibleIf
{ typedef typename _IsCVConvertible<_Src, _Dst>::_Ret _Ret; };

// type_manips.h
template 
struct _IsCVConvertible {
  typedef _ConversionHelper<_Src, _Dst> _H;
  enum { value = (sizeof(char) == sizeof(_H::_Test(false, _H::_MakeSource()))) };
  typedef typename __bool2type::_Ret _Ret;
};

template 
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 
#include 
#include 

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 v_derived;
    vector 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。看看它的源代码是怎么实现的吧:

// xutility
template
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 inline
typename _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
struct _Ptr_cat_helper<_Ty **, _Ty **>
    {   // return pointer category from pointer to pointer arguments
    typedef _Scalar_ptr_iterator_tag _Ptr_cat;    // 可以memmove
    };

template
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
struct _Ptr_cat_with_checked_cat_helper
    {
    typedef _Nonscalar_ptr_iterator_tag _Ptr_cat;    // 不可以memmove
    };

template
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
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++的多重继承真是个大陷阱。

你可能感兴趣的:(STLport,copy,指针,多重继承,C++)