【问题标题】:stringstream unsigned input validationstringstream 无符号输入验证
【发布时间】:2013-09-20 12:46:37
【问题描述】:

我正在编写程序的一部分,它解析和验证程序控制台参数中的一些用户输入。为此,我选择使用 stringstream,但在读取无符号类型时遇到了问题。

下一个模板用于从给定字符串中读取请求的类型:

#include <iostream>
#include <sstream>
#include <string>

using std::string;
using std::stringstream;
using std::cout;
using std::endl;

template<typename ValueType>
ValueType read_value(string s)
{   
    stringstream ss(s);
    ValueType res;
    ss >> res;
    if (ss.fail() or not ss.eof())
        throw string("Bad argument: ") + s;
    return res;
}
// +template specializations for strings, etc. 

int main(void)
{   
    cout << read_value<unsigned int>("-10") << endl;
}   

如果类型是无符号的并且输入字符串包含负数,我希望看到异常抛出(由ss.fail() = true 引起)。但是 stringstream 会生成转换为无符号类型的值(书面示例中为 4294967286)。

如何修复此示例以实现所需的行为(最好不要回退到 c 函数)? 我知道它可以通过简单的第一个符号检查来完成,但我可以放置前导空格。我可以编写自己的解析器,但不相信问题如此难以预测,标准库无法解决。

对于无符号类型,隐藏在 stringstream 运算符深处的函数是 strtoull 和 strtoul。它们以描述的方式工作,但提到的功能是低级的。为什么 stringstream 不提供一些验证级别? (我只是希望我错了,它确实如此,但需要一些动作才能启用它)。

【问题讨论】:

  • 我看到了这个线程,但是在不同的编译器中存在不一致的行为问题。最后,据我所知,MS VS 和现代 GCC 上的行为只是转换为无符号表示。
  • 重新打开; linked duplicate 仅针对 C++03,但在 C++11 中行为发生了变化。

标签: c++ validation c++11


【解决方案1】:

版本免责声明:C++03 的答案不同。以下涉及 C++11。

首先,让我们分析一下发生了什么。

ss &gt;&gt; res; 这调用了std::istream::operator&gt;&gt;(unsigned)。在[istream.formatted.arithmetic]/1中,效果定义如下:

这些提取器表现为格式化输入函数(如 27.7.2.2.1 中所述)。在构造一个哨兵对象后,转换就像由以下代码片段执行一样:

typedef num_get< charT,istreambuf_iterator<charT,traits> > numget;
iostate err = iostate::goodbit;
use_facet< numget >(loc).get(*this, 0, *this, err, val);
setstate(err);

在上面的片段中,loc 代表basic_ios 类的私有成员。

格式化输入函数 到 [istream::sentry] 之后,sentry 对象的主要作用是消耗前导空白字符。如果出现错误(流处于失败/eof 状态),它还会阻止执行上面显示的代码。

使用的语言环境是"C" 语言环境。理由:

对于通过stringstream ss(s); 构造的stringstream,该iostream 的语言环境是构造时的当前全局语言环境(这在[ios.base.locales]/4 的兔子洞深处得到保证) .由于 OP 程序中的全局语言环境未更改,因此 [locale.cons]/2 指定“经典”语言环境,即 "C" 语言环境。

use_facet&lt; numget &gt;(loc).get 使用 [locale.num.get] 中指定的成员函数num_get&lt;char&gt;::get(iter_type in, iter_type end, ios_base&amp;, ios_base::iostate&amp; err, unsigned int&amp; v) const;(注意unsigned int,一切正常)。 “C”语言环境的字符串 -> unsigned int 转换的详细信息很长,并在 [facet.num.get.virtuals] 中进行了描述。一些有趣的细节:

  • 对于无符号整数值,使用函数strtoull
  • 如果转换失败,ios_base::failbit 将分配给err。具体来说:“要存储的数值可以是以下之一:[...] 可表示的最负值或无符号整数类型为零,如果该字段表示的值太大而无法以 val 表示。ios_base::failbit分配给err。”

我们需要去C99,7.20.1.4的第5段下strtoull的定义:

如果主题序列以减号开头,则从 转换被否定(在返回类型中)。

根据第 8 段:

如果正确值超出可表示值的范围,则返回LONG_MINLONG_MAXLLONG_MINLLONG_MAXULONG_MAXULLONG_MAX(根据返回类型和符号值,如果有的话),宏ERANGE的值存储在errno

过去似乎一直在争论是否将负值视为strotoul 的有效输入。无论如何,问题出在这个函数上。对 gcc 的快速检查表明它被视为有效输入,因此是您观察到的行为。


历史记录:C++03

C++03 在num_get 转换中使用了scanf。不幸的是,我还不太确定scanf 的转换是如何指定的,以及在什么情况下会发生错误。


显式错误检查:

我们可以手动插入该检查,方法是使用带符号的值进行转换并测试&lt;0,或者我们寻找- 字符(由于可能存在本地化问题,这不是一个好主意)。

【讨论】:

  • 依我的拙见,负值应该被视为strtoul 的有效输入,因为在 7.20.1.4 中会考虑值的符号进行错误检查/8。但我不想和 C 人吵架 ;)
  • 很遗憾,但首先使用有符号类型并不能治愈:例如ValueType = char 有符号和无符号类型之间的差异很可能会随着用户交互而增加。所以,我们可以使用 most-long 类型,但在很多情况下它是多余的。并且模板的灵活性将丢失。根据this,第 2 阶段有字符验证。如果 is_signed 失败,为什么不限制- 的出现?或者类似的东西?
  • @AlexanderSergeev 是的,您不能直接使用无符号类型的有符号等价物。 (在您的示例中,"128" 不是 8 位 signed char 的有效输入。)您必须使用比原始无符号类型具有更大正范围的有符号整数类型。这可以通过元编程来完成,但并不漂亮。
  • “为什么不限制 - 如果 is_signed 失败则显示?”你确实可以为你自己的 locale/num_get facet 这样做。我试着举个例子。
  • 类型容量增长有一些限制。例如,就我们使用通用(元)编程而言,我们必须正确处理unsigned long long 类型。我相信在这种情况下打开长算术不是一个选择。
【解决方案2】:

num_get facet 支持显式检查签名。拒绝任何以 '-'(在空格之后)开头的非零数字用于无符号类型,并使用默认 C 语言环境的 num_get 进行实际转换。

#include <locale>
#include <istream>
#include <ios>
#include <algorithm>

template <class charT, class InputIterator = std::istreambuf_iterator<charT> >
class num_get_strictsignedness : public std::num_get <charT, InputIterator>
{
public:
    typedef charT char_type;
    typedef InputIterator iter_type;

    explicit num_get_strictsignedness(std::size_t refs = 0)
        : std::num_get<charT, InputIterator>(refs)
    {}
    ~num_get_strictsignedness()
    {}

private:
    #define DEFINE_DO_GET(TYPE) \
        virtual iter_type do_get(iter_type in, iter_type end,      \
            std::ios_base& str, std::ios_base::iostate& err,       \
            TYPE& val) const override                              \
        {  return do_get_templ(in, end, str, err, val);  }         // MACRO END

    DEFINE_DO_GET(unsigned short)
    DEFINE_DO_GET(unsigned int)
    DEFINE_DO_GET(unsigned long)
    DEFINE_DO_GET(unsigned long long)

    // not sure if a static locale::id is required..

    template <class T>
    iter_type do_get_templ(iter_type in, iter_type end, std::ios_base& str,
                           std::ios_base::iostate& err, T& val) const
    {
        using namespace std;

        if(in == end)
        {
            err |= ios_base::eofbit;
            return in;
        }

        // leading white spaces have already been discarded by the
        // formatted input function (via sentry's constructor)

        // (assuming that) the sign, if present, has to be the first character
        // for the formatting required by the locale used for conversion

        // use the "C" locale; could use any locale, e.g. as a data member

        // note: the signedness check isn't actually required
        //       (because we only overload the unsigned versions)
        bool do_check = false;
        if(std::is_unsigned<T>{} && *in == '-')
        {
            ++in;  // not required
            do_check = true;
        }

        in = use_facet< num_get<charT, InputIterator> >(locale::classic())
                 .get(in, end, str, err, val);

        if(do_check && 0 != val)
        {
            err |= ios_base::failbit;
            val = 0;
        }

        return in;
    }
};

使用示例:

#include <sstream>
#include <iostream>
int main()
{
    std::locale loc( std::locale::classic(),
                     new num_get_strictsignedness<char>() );
    std::stringstream ss("-10");
    ss.imbue(loc);
    unsigned int ui = 42;
    ss >> ui;
    std::cout << "ui = "<<ui << std::endl;
    if(ss)
    {
        std::cout << "extraction succeeded" << std::endl;
    }else
    {
        std::cout << "extraction failed" << std::endl;
    }
}

注意事项:

  • 免费商店的分配不是必需的,您可以使用例如一个(静态)局部变量,您可以在其中使用 ctor 中的 1 初始化 ref 计数器
  • 对于您想要支持的每种字符类型(如 charwchar_tcharXY_t),您需要添加自己的构面(可以是 num_get_strictsignedness 模板的不同实例化)
  • "-0" 被接受

【讨论】:

  • 我不明白为什么有人喜欢输入/输出和语言环境库 [/sarcasm]
  • 一切都很棒!除了gcc bugtracker 中提到的小事 - 它拒绝“-0”字符串。我并不是说你应该修复它,只是做个笔记。感谢您的回答,将尝试使用/升级它。
  • @AlexanderSergeev 修复了它;)(虽然它与 gcc 错误无关,但它很相似,而且很容易修复)
  • 真的,很容易修复:)
  • @AlexanderSergeev 注意我刚刚发现eofbit 被覆盖了。修好了。
猜你喜欢
  • 2015-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多