天天看点

C++流实现内幕---由boost::lexical_cast引发的一个问题

中午同事碰见一个关于使用boost::lexical_cast产生异常的问题,关键代码如下

string str(8,'/0');

strncpy(&str.at(0),"1234567",7);

cout << lexical_cast<int>(str) << endl;

结果运行的时候发生如下异常

terminate called after throwing an instance of 'boost::bad_lexical_cast'

  what():  bad lexical cast: source type value could not be interpreted as target

我们知道boost::lexical_cast最终使用的stringstream实现的数值类型转换,所以,我们使用如下例子,做测试

stringstream ss;

ss << str;

ss >> result;

cout << "new result: " << result << endl;

编译运行后,输出

new result: 1234567

可以正常显示,好像没有问题,

我们察看一下boost的源代码

vim /usr/include/boost/lexical_cast.hpp

察看lexical_cast函数

template<typename target, typename source>

    target lexical_cast(source arg)

    {

        detail::lexical_stream<target, source> interpreter;

        target result;

        if(!(interpreter << arg && interpreter >> result))

            throw_exception(bad_lexical_cast(typeid(target), typeid(source)));

        return result;

}

可见lexical_cast函数非常简单,就是具体执行operator<<和operator>>两个操作,只要这两个操作有一个失败就抛出一个异常,为了确认是那步出的错,我们在程序中手工执行这两个操作。代码如下

detail::lexical_stream<int, string> interpreter;

    int result;

    if(!(interpreter << str ))

        cout << "error 1" << endl;

    }

    if(!(interpreter >> result))

        cout << "error 2" << endl;

cout << result << endl;

编译运行后输出

error 2

从这里我们知道,lexical_cast是在执行输出流的时候发生的问题,察看detail的operator>>函数,其源代码如下

template<typename inputstreamable>

            bool operator>>(inputstreamable &output)

            {

                return !is_pointer<inputstreamable>::value &&

                       stream >> output &&

                       (stream >> std::ws).eof();

            }

根据以上代码和我们使用stringstream做的测试,基本上可以确定在stream>>output(包括次步)都是正确的,可能出现问题的是(stream >>

std::ws).eof();

这里解释下std::ws和stringstring::eof()函数

std::ws函数声明在

/usr/include/c++/3.4.4/bits/istream.tcc

源代码如下

// 27.6.1.4 standard basic_istream manipulators

  template<typename _chart, typename _traits>

    basic_istream<_chart,_traits>&

    ws(basic_istream<_chart,_traits>& __in)

      typedef basic_istream<_chart, _traits>        __istream_type;

      typedef typename __istream_type::__streambuf_type __streambuf_type;

      typedef typename __istream_type::__ctype_type __ctype_type;

      typedef typename __istream_type::int_type     __int_type;

      const __ctype_type& __ct = use_facet<__ctype_type>(__in.getloc());

      const __int_type __eof = _traits::eof();

      __streambuf_type* __sb = __in.rdbuf();

      __int_type __c = __sb->sgetc();

      while (!_traits::eq_int_type(__c, __eof)

         && __ct.is(ctype_base::space, _traits::to_char_type(__c)))

    __c = __sb->snextc();

       if (_traits::eq_int_type(__c, __eof))

     __in.setstate(ios_base::eofbit);

      return __in;

主要作用是过滤输入流中的空格,/n/r等字符。stream

>> std::ws目的就是把输入流中转换完整形后的剩余流内容(假如有的话)写入std::ws,当然只能写入其中的空格和/n/r等字符。

stringstring::eof()函数参考 http://www.cppreference.com/wiki/io/eof

部分

该函数的主要作用是,如果到达流的结束位置返回true,否则返回false

根据以上信息,我们编写测试用例

cout << ss.eof() << endl;

cout << (ss >> std::ws).eof() << endl;

由此可见,虽然我们使用ss时,可以输出想要的正确结果,但是我们缺少最后的安全验证,而boost::lexical_cast就做了这方面的验证。

其实例子中的’/0’在开始的时候,起了不小的误导作用,开始以为是boost::lexical_cast无法处理最后末尾是’/0’的字符串,到现在其实不然,我们把’/0’转换为’a’字符一样会出现这种问题,但是我们使用’/n’,’/r’和空格等字符就不会出现这种问题,现在我们知道其根源就是在字符转换过程中输入流没有输入全部字符,所以流的结束标志eof,一直为0。

其实在上面的应用中我们不能一直认为boost::lexical_cast的方法一定是好的。在我们编成过程中,常见的转换是把一段字符串中含有数字和字母的字符串中的数字串转换为整形,这样的如果我们使用boost::lexical_cast的话,永远得不到正确结果了,每次都会有异常抛出,这时候我们可以使用stringstream,转换后不判断eof(),这样就可以得到我们想要的整数。

       在上面的测试中,突然想到一个变态的想法,stl中的字符串转为整形的流实现是怎么做的,不过sgi的stl真够难堪的。

       大体查找过程如下

       (1): vim

/usr/include/c++/3.4.4/sstream

发现引用了istream

       (2):  vim

/usr/include/c++/3.4.4/ istream

发现operator<<(int)的实现在bits/istream.tcc文件中

       (3): vim

/usr/include/c++/3.4.4/ bits/istream.tcc

发现const

__num_get_type& __ng = __check_facet(this->_m_num_get);__ng.get(*this,

0, *this, __err, __l);所以查找__num_get_type类型中的get函数,同时发现istream.tcc中的#include <locale> 比较陌生,同时在istream中查找__num_get_type 类型为typedef num_get<_chart,

istreambuf_iterator<_chart, _traits> > __num_get_type; 所以,最终要查找的类型为num_get

        (4): vim /usr/include/c++/3.4.4/locale

发现这个文件中包括以下头文件

#include <bits/localefwd.h>

#include <bits/locale_classes.h>

#include <bits/locale_facets.h>

#include <bits/locale_facets.tcc>

逐个察看

       (5):  vim

/usr/include/c++/3.4.4/ bits/localefwd.h

发现模板类num_get声明

template<typename _chart, typename

_initer = istreambuf_iterator<_chart> >

class num_get;

       (6): vim

/usr/include/c++/3.4.4/

bits/locale_facets.h

在这个文件中发现num_get的实现

_initer>

class num_get :

public locale::facet

查找get方法

iter_type

get(iter_type __in, iter_type __end, ios_base& __io,

ios_base::iostate& __err, bool& __v) const

{ return this->do_get(__in, __end, __io, __err, __v); }

查找do_get方法

       (7):  vim /usr/include/c++/3.4.4/

bits/locale_facets.tcc

发现

// _glibcxx_resolve_lib_defects

  // 17.  bad bool parsing

  template<typename _chart, typename _initer>

    _initer

    num_get<_chart, _initer>::

    do_get(iter_type __beg, iter_type __end, ios_base& __io,

           ios_base::iostate& __err, bool& __v) const

      if (!(__io.flags() & ios_base::boolalpha))

        {

      // parse bool values as long.

          // nb: we can't just call do_get(long) here, as it might

          // refer to a derived class.

      long __l = -1;

          __beg = _m_extract_int(__beg, __end, __io, __err, __l);

      if (__l == 0 || __l == 1)

        __v = __l;

      else

...

查找_m_extract_int

方法

终于找到

 template<typename _chart, typename _initer>

    template<typename _valuet>

      _initer

      num_get<_chart, _initer>::

      _m_extract_int(_initer __beg, _initer __end, ios_base& __io,

             ios_base::iostate& __err, _valuet& __v) const

      {

        typedef char_traits<_chart>         __traits_type;

    typedef typename numpunct<_chart>::__cache_type __cache_type;

    __use_cache<__cache_type> __uc;

    const locale& __loc = __io._m_getloc();

    const __cache_type* __lc = __uc(__loc);

const _chart* __lit = __lc->_m_atoms_in;

....

分析_m_extract_int的关键代码,

如下

int __base = __oct ? 8 : (__basefield == ios_base::hex ? 16 : 10);

          const _valuet __new_result = __result * __base

                                         - __digit;

            __overflow |= __new_result > __result;

            __result = __new_result;

            ++__sep_pos;

            __found_num = true;

根据以上代码c++中的流转换,没有使用什么特别的技巧,在由字符串转为数字时,使用的也是查找字符*10(8,16)的方法,只是这个过程中多了很多步我们想不到的安全验证。

总算搞明白了,sgi真不是给人看得,你也可以了解float类型是怎么实现的,参考_m_extract_float函数。

继续阅读