有效地从字符串中读取两个逗号分隔的浮点数,而不受全局语言环境的影响

Efficiently reading two comma-separated floats in brackets from a string without being affected by the global locale

本文关键字:浮点数 影响 环境 语言 全局 分隔 读取 字符串 两个 有效地      更新时间:2023-10-16

我是一个库的开发人员,我们的旧代码使用sscanf()sprintf()从/到字符串读写各种内部类型。有些用户使用我们的库,但他们的语言环境与我们基于XML文件的语言环境("C"语言环境)不同,我们遇到了一些问题。在我们的示例中,这会导致从那些XML文件解析出的不正确的值以及在运行时作为字符串提交的值。区域设置可以由用户直接更改,但也可以在用户不知情的情况下更改。如果区域设置更改发生在另一个库中,例如GTK,它是一个错误报告中的"肇事者",则可能发生这种情况。因此,我们显然想要从区域设置中删除任何依赖,以永久地摆脱这些问题。

我已经在float/double/int/…上下文中阅读了其他问题和答案。特别是如果它们被字符分隔或位于括号内,但到目前为止,我找到的建议解决方案并不令我们满意。我们的要求是:

  1. 不依赖于标准库以外的库。例如,使用boost中的任何东西都不是一个选项。

  2. 必须是线程安全的。这是特定于区域设置的,它可以全局更改。这对我们来说真的很糟糕,因为我们库中的一个线程可能会受到用户程序中的另一个线程的影响,而这个线程也可能正在运行一个完全不同库的代码。因此,任何直接受setlocale()影响的东西都不是一个选择。此外,在开始读/写之前设置区域设置,然后将其设置回原始值,由于线程中的竞争条件,这不是一个解决方案。

  3. 虽然效率不是最优先考虑的(#1 ),它仍然是我们所关心的,因为字符串可能在运行时非常频繁地读写,这取决于用户的程序。

Edit:作为额外的注意:boost::lexical_cast不保证不受语言环境的影响(来源:boost::lexical_cast<>的语言环境不变性保证)。因此,即使没有要求#1,这也不是一个解决方案。

到目前为止,我收集了以下信息:

  • 首先,我看到很多人建议使用boost的lexical_cast,但不幸的是,这根本不是我们的选择,因为我们不能要求所有用户也链接到boost(因为缺乏区域安全,见上文)。我查看了代码,看看我们是否可以从中提取任何东西,但我发现它很难理解,而且长度太大,而且最有可能的是,大的性能增益是使用依赖于语言环境的函数。
  • c++ 11中引入的许多函数,如std::to_string, std::stod, std::stof等,就像sscanf和sprintf一样依赖于全局语言环境,这是非常不幸的,对我来说是不可理解的,考虑到std::thread已经被添加。一般来说,
  • std::stringstream似乎是一种解决方案,因为它在区域设置上下文中是线程安全的,但通常也是正确保护的。但是,如果每次都是新构建,则速度会很慢(很好的比较:http://www.boost.org/doc/libs/1_55_0/doc/html/boost_lexical_cast/performance.html)。我认为这可以通过为每个线程配置一个这样的流来解决,并在每次使用后清除它。然而,一个问题是,它不像sscanf()那样容易解决格式,例如:" { %g , %g } "
例如,我们需要能够读取的sscanf()模式是:

  • " { %g , %g }"
  • " { { %g , %g } , { %g , %g } }"
  • " { top: { %g , %g } , left: { %g , %g } , bottom: { %g , %g } , right: { %g , %g }"

用stringstreams编写这些似乎没什么大不了的,但是阅读它们似乎有问题,特别是考虑到空白。

我们应该在这种情况下使用std::regex吗?stringstreams是这个任务的一个很好的解决方案,还是有更好的方法来实现上述要求?另外,在线程安全和区域设置的上下文中,是否还有我在问题中没有考虑到的其他问题-特别是关于std::stringstream的使用?

在您的情况下,stringstream似乎是最好的方法,因为您可以独立于所设置的全局区域设置控制它的区域设置。但是,格式化的阅读确实不像sscanf()那样容易。

从性能的角度来看,使用regex的流输入对于这种简单的逗号分隔读取来说是多余的:在非正式的基准测试中,它比scanf()慢10倍以上。

可以很容易地编写一个小的辅助类来帮助读取像枚举这样的格式。下面是另一个SO答案的大意,用法很简单:

sst >> mandatory_input(" { ")>> x >> mandatory_input(" , ")>>y>> mandatory_input(" } ");

如果你感兴趣的话,我以前写过一篇。这里是完整的文章,包括示例和解释以及源代码。这个类有70行代码,但其中大部分是为了在需要时提供错误处理函数。它具有可接受的性能,但仍然比scanf()慢。

根据Christophe的建议和我找到的其他一些stackoverflow答案,我创建了一组2个方法和1个类来实现我们所需的所有流解析功能。以下方法足以解析问题中提出的格式:

以下方法去掉前面的空格,然后跳过一个可选字符:

template<char matchingCharacter>
std::istream& optionalChar(std::istream& inputStream)
{
    if (inputStream.fail())
        return inputStream;
    inputStream >> std::ws;
    if (inputStream.peek() == matchingCharacter)
        inputStream.ignore();
    else
        // If peek is executed but no further characters remain,
        // the failbit will be set, we want to undo this
        inputStream.clear(inputStream.rdstate() & ~std::ios::failbit);
    return inputStream;
}

第二个方法去掉前面的空格,然后检查是否有强制字符。如果不匹配,则设置失败位:

template<char matchingCharacter>
std::istream& mandatoryChar(std::istream& inputStream)
{
    if (inputStream.fail())
        return inputStream;
    inputStream >> std::ws;
    if (inputStream.peek() == matchingCharacter)
        inputStream.ignore();
    else
        inputStream.setstate(std::ios_base::failbit);
    return inputStream;
}

使用全局stringstream(在每次使用之前调用strStream.str(std::string())clear())来提高性能是有意义的,正如我的问题所暗示的那样。使用可选的字符检查,我可以使解析对其他样式更加宽松。下面是一个用法示例:

// Format is: " { { %g , %g } , { %g , %g } } " but we are lenient regarding the format,
// so this is also allowed: " { %g %g } { %g %g } "
std::stringstream sstream(inputString);
sstream.clear();
sstream >> optionalChar<'{'> >> mandatoryChar<'{'> >> val1 >>
    optionalChar<','> >> val2 >>
    mandatoryChar<'}'> >> optionalChar<','> >> mandatoryChar<'{'> >> val3 >>
    optionalChar<','> >> val4;
if (sstream.fail())
    logError(inputString);

添加-检查强制字符串:

最后但并非最不重要的是,基于Christophe的想法,我从零开始创建了一个类来检查流中的强制字符串。头文件:

class MandatoryString
{
public:
    MandatoryString(char const* mandatoryString);
    friend std::istream& operator>> (std::istream& inputStream, const MandatoryString& mandatoryString);  
private:
    char const* m_chars;
};

Cpp文件:

MandatoryString::MandatoryString(char const* mandatoryString)
    : m_chars(mandatoryString)
{}
std::istream& operator>> (std::istream& inputStream, const MandatoryString& mandatoryString) 
{
    if (inputStream.fail())
        return inputStream;
    char const* currentMandatoryChar = mandatoryString.m_chars;
    while (*currentMandatoryChar != '')
    {
        static const std::locale spaceLocale("C");
        if (std::isspace(*currentMandatoryChar, spaceLocale))
        {
            inputStream >> std::ws;
        }
        else
        {
            int peekedChar = inputStream.get();
            if (peekedChar != *currentMandatoryChar)
            {
                inputStream.setstate(std::ios::failbit); 
                break;
            }
        }
        ++currentMandatoryChar;
    }
    return inputStream;
}

MandatoryString类的用法与上述方法类似,例如:

sstream >> MandatoryString(" left");

结论:虽然此解决方案可能比sscanf更冗长,但它在能够使用stringstreams时为我们提供了所需的所有灵活性,这使得该解决方案通常是线程安全的,并且不依赖于全局语言环境。此外,它很容易检查错误,一旦设置了失败位,解析将在建议的方法中停止。对于要在字符串中解析的非常长的值序列,这实际上比sscanf更具可读性:例如,它允许跨多行拆分解析,前面的强制字符串分别与相应的变量在同一行。n o e T h̶̶̶̶̶̶y l̶̶̶T r p̶̶̶̶̶T T h̶̶̶̶̶̶̶o d e s̶̶̶n o T̶̶̶̶k w o r̶̶̶̶̶̶n y l e c我̶̶̶̶̶̶w h T我̶̶̶̶̶̶̶h T我̶̶̶s T u l o̶̶̶̶̶̶̶o n̶̶̶s̶̶̶̶̶r s p g n我̶̶̶̶̶̶m u l̶̶T e l p我̶̶̶̶̶̶̶̶h e c e d x̶̶̶̶̶我̶̶̶l s̶̶̶̶f r m o̶̶̶̶̶o n e̶̶̶s r T̶̶̶̶̶̶n g,̶̶h w c h我̶̶̶̶̶̶̶̶e r e r问我你̶̶̶̶̶̶年代一个̶̶̶̶e c o d n̶̶̶̶̶̶̶s m T e r̶̶̶̶̶̶̶̶n d̶̶̶一l o T̶̶̶̶̶o f̶̶̶̶̶d d我T̶̶̶̶l o n̶̶̶̶̶̶l我n e̶̶̶̶̶̶̶o f e c o d̶̶̶̶̶̶̶o f̶̶r c e l̶̶̶̶̶̶n g̶̶̶̶n d̶̶̶̶̶e g T e l n我̶̶̶̶̶̶l l c̶̶̶̶̶。重载流操作符<<而>>对于我们的内部类型,一切看起来都很干净,很容易维护。解析多个十六进制也可以正常工作,只需在操作完成后将先前设置的std::hex值重置为std::dec。