有效地从字符串中读取括号中的两个逗号分隔的浮点数,而不受全局语言环境的影响
Efficiently reading two comma-separated floats in brackets from a string without being affected by the global locale
我是一个库的开发人员,我们的旧代码使用 sscanf()
和 sprintf()
到 read/write 各种内部类型 from/to 字符串。我们遇到过使用我们的库的用户的问题,他们的语言环境与我们基于 XML 文件的语言环境不同("C" 语言环境)。在我们的例子中,这导致从那些 XML 文件和在 运行 时间内作为字符串提交的文件解析的值不正确。区域设置可以由用户直接改变,但也可以在用户不知情的情况下改变。如果 locale-changes 发生在另一个库中,例如 GTK,这是一个错误报告中的 "perpetrator",就会发生这种情况。因此,我们显然希望从语言环境中删除任何依赖项,以永久摆脱这些问题。
我已经阅读了 float/double/int/... 上下文中的其他问题和答案,特别是如果它们由字符分隔或位于括号内,但到目前为止,我发现的建议解决方案并不令人满意给我们。我们的要求是:
不依赖标准库以外的库。因此,例如,使用任何来自 boost 的东西都不是一种选择。
必须是线程安全的。这意味着特定于区域设置,可以在全球范围内更改。这对我们来说真的很糟糕,因为我们库的一个线程可能会受到用户程序中另一个线程的影响,这也可能是 运行 完全不同库的代码。因此,任何受 setlocale()
直接影响的东西都不是一个选项。此外,在开始之前将区域设置设置为 read/write 并在之后将其设置回原始值不是解决方案,因为线程中存在竞争条件。
虽然效率不是最重要的(#1 和#2 是),但它仍然是我们关注的焦点,因为字符串的读写时间可能相当 运行经常,这取决于用户的程序。越快越好
编辑: 作为附加说明:boost::lexical_cast
不能保证不受语言环境的影响(来源:Locale invariant guarantee of boost::lexical_cast<>)。因此,即使没有要求 #1,这也不是解决方案。
到目前为止我收集了以下信息:
- 首先,我看到很多人建议使用 boost 的 lexical_cast 但不幸的是,这根本不是我们的选择,因为我们不能要求所有用户也 link 来提升(并且由于缺乏区域设置安全性,请参见上文)。我查看了代码,看看我们是否可以从中提取任何内容,但我发现它很难理解,而且篇幅太大,而且很可能大型性能提升者无论如何都在使用依赖于语言环境的函数。
- 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 }"
用字符串流编写这些似乎没什么大不了的,但读取它们似乎有问题,尤其是考虑到空格。
我们应该在这种情况下使用 std::regex
还是这太过分了? stringstreams 是完成此任务的良好解决方案,还是有更好的方法来满足上述要求?另外,在我的问题中没有考虑到线程安全和语言环境方面的任何其他问题 - 特别是关于 std::stringstream?
的使用
在您的情况下,stringstream
似乎是最好的方法,因为您可以独立于设置的全局区域设置来控制它的区域设置。但是格式化阅读确实不如sscanf()
那么容易。
从性能的角度来看,使用正则表达式的流输入对于这种简单的逗号分隔阅读来说是一种矫枉过正:在非正式基准测试中,它比 scanf() 慢 10 倍以上。
你可以很容易地写一个小辅助class来方便阅读你列举的格式。这里关于 another SO answer 的总体思路使用起来很简单:
sst >> mandatory_input(" { ")>> x >> mandatory_input(" , ")>>y>> mandatory_input(" } ");
如果你有兴趣,我前段时间写过一篇。这里是full article with examples and explanation as well as source code。 class 是 70 行代码,但其中大部分代码提供了错误处理功能,以备不时之需。它具有可接受的性能,但仍然比 scanf() 慢。
根据 Christophe 的建议和我发现的其他一些 Whosebug 答案,我创建了一组 2 个方法和 1 个 class 来实现我们需要的所有流解析功能。以下方法足以解析问题中提出的格式:
以下方法去除前面的空格,然后跳过一个可选字符:
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;
}
使用全局字符串流(在每次使用前调用 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 用于从头开始检查流中的强制字符串。头文件:
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 != '[=14=]')
{
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
class的用法与上述方法类似,例如:
sstream >> MandatoryString(" left");
结论:
虽然此解决方案可能比 sscanf 更冗长,但它在能够使用字符串流的同时为我们提供了所需的所有灵活性,这使得此解决方案通常是线程安全的并且不依赖于全局区域设置。此外,检查错误也很容易,一旦设置了失败位,解析将在建议的方法内停止。对于要在字符串中解析的非常长的值序列,这实际上比 sscanf 更具可读性:例如,它允许将解析拆分为多行,前面的强制字符串与相应的变量位于同一行,respectively.T̶h̶e̶ ̶o̶n̶l̶y̶ ̶p̶a̶r̶t̶ ̶t̶h̶a̶t̶ ̶d̶o̶e̶s̶ ̶n̶o̶t̶ ̶w̶o̶r̶k̶ ̶n̶i̶c̶e̶l̶y̶ ̶w̶i̶t̶h̶ ̶t̶h̶i̶s̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶p̶a̶r̶s̶i̶n̶g̶ ̶m̶u̶l̶t̶i̶p̶l̶e̶ ̶h̶e̶x̶a̶d̶e̶c̶i̶m̶a̶l̶s̶ ̶f̶r̶o̶m̶ ̶o̶n̶e̶ ̶s̶t̶r̶i̶n̶g̶,̶ ̶w̶h̶i̶c̶h̶ ̶r̶e̶q̶u̶i̶r̶e̶s̶ ̶a̶ ̶s̶e̶c̶o̶n̶d̶ ̶s̶t̶r̶e̶a̶m̶ ̶a̶n̶d̶ ̶a̶ ̶l̶o̶t̶ ̶o̶f̶ ̶a̶d̶d̶i̶t̶i̶o̶n̶a̶l̶ ̶l̶i̶n̶e̶s̶ ̶o̶f̶ ̶c̶o̶d̶e̶ ̶o̶f̶ ̶c̶l̶e̶a̶r̶i̶n̶g̶ ̶a̶n̶d̶ ̶g̶e̶t̶L̶i̶n̶e̶ ̶c̶a̶l̶l̶s̶.̶ After overloading the stream operators << and >> 对于我们的内部类型,一切看起来都很干净并且易于维护。解析多个十六进制也可以正常工作,我们只是在操作完成后将之前设置的std::hex值重置为std::dec。
我是一个库的开发人员,我们的旧代码使用 sscanf()
和 sprintf()
到 read/write 各种内部类型 from/to 字符串。我们遇到过使用我们的库的用户的问题,他们的语言环境与我们基于 XML 文件的语言环境不同("C" 语言环境)。在我们的例子中,这导致从那些 XML 文件和在 运行 时间内作为字符串提交的文件解析的值不正确。区域设置可以由用户直接改变,但也可以在用户不知情的情况下改变。如果 locale-changes 发生在另一个库中,例如 GTK,这是一个错误报告中的 "perpetrator",就会发生这种情况。因此,我们显然希望从语言环境中删除任何依赖项,以永久摆脱这些问题。
我已经阅读了 float/double/int/... 上下文中的其他问题和答案,特别是如果它们由字符分隔或位于括号内,但到目前为止,我发现的建议解决方案并不令人满意给我们。我们的要求是:
不依赖标准库以外的库。因此,例如,使用任何来自 boost 的东西都不是一种选择。
必须是线程安全的。这意味着特定于区域设置,可以在全球范围内更改。这对我们来说真的很糟糕,因为我们库的一个线程可能会受到用户程序中另一个线程的影响,这也可能是 运行 完全不同库的代码。因此,任何受
setlocale()
直接影响的东西都不是一个选项。此外,在开始之前将区域设置设置为 read/write 并在之后将其设置回原始值不是解决方案,因为线程中存在竞争条件。虽然效率不是最重要的(#1 和#2 是),但它仍然是我们关注的焦点,因为字符串的读写时间可能相当 运行经常,这取决于用户的程序。越快越好
编辑: 作为附加说明:boost::lexical_cast
不能保证不受语言环境的影响(来源:Locale invariant guarantee of boost::lexical_cast<>)。因此,即使没有要求 #1,这也不是解决方案。
到目前为止我收集了以下信息:
- 首先,我看到很多人建议使用 boost 的 lexical_cast 但不幸的是,这根本不是我们的选择,因为我们不能要求所有用户也 link 来提升(并且由于缺乏区域设置安全性,请参见上文)。我查看了代码,看看我们是否可以从中提取任何内容,但我发现它很难理解,而且篇幅太大,而且很可能大型性能提升者无论如何都在使用依赖于语言环境的函数。
- 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 }"
用字符串流编写这些似乎没什么大不了的,但读取它们似乎有问题,尤其是考虑到空格。
我们应该在这种情况下使用 std::regex
还是这太过分了? stringstreams 是完成此任务的良好解决方案,还是有更好的方法来满足上述要求?另外,在我的问题中没有考虑到线程安全和语言环境方面的任何其他问题 - 特别是关于 std::stringstream?
在您的情况下,stringstream
似乎是最好的方法,因为您可以独立于设置的全局区域设置来控制它的区域设置。但是格式化阅读确实不如sscanf()
那么容易。
从性能的角度来看,使用正则表达式的流输入对于这种简单的逗号分隔阅读来说是一种矫枉过正:在非正式基准测试中,它比 scanf() 慢 10 倍以上。
你可以很容易地写一个小辅助class来方便阅读你列举的格式。这里关于 another SO answer 的总体思路使用起来很简单:
sst >> mandatory_input(" { ")>> x >> mandatory_input(" , ")>>y>> mandatory_input(" } ");
如果你有兴趣,我前段时间写过一篇。这里是full article with examples and explanation as well as source code。 class 是 70 行代码,但其中大部分代码提供了错误处理功能,以备不时之需。它具有可接受的性能,但仍然比 scanf() 慢。
根据 Christophe 的建议和我发现的其他一些 Whosebug 答案,我创建了一组 2 个方法和 1 个 class 来实现我们需要的所有流解析功能。以下方法足以解析问题中提出的格式:
以下方法去除前面的空格,然后跳过一个可选字符:
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;
}
使用全局字符串流(在每次使用前调用 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 用于从头开始检查流中的强制字符串。头文件:
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 != '[=14=]')
{
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
class的用法与上述方法类似,例如:
sstream >> MandatoryString(" left");
结论: 虽然此解决方案可能比 sscanf 更冗长,但它在能够使用字符串流的同时为我们提供了所需的所有灵活性,这使得此解决方案通常是线程安全的并且不依赖于全局区域设置。此外,检查错误也很容易,一旦设置了失败位,解析将在建议的方法内停止。对于要在字符串中解析的非常长的值序列,这实际上比 sscanf 更具可读性:例如,它允许将解析拆分为多行,前面的强制字符串与相应的变量位于同一行,respectively.T̶h̶e̶ ̶o̶n̶l̶y̶ ̶p̶a̶r̶t̶ ̶t̶h̶a̶t̶ ̶d̶o̶e̶s̶ ̶n̶o̶t̶ ̶w̶o̶r̶k̶ ̶n̶i̶c̶e̶l̶y̶ ̶w̶i̶t̶h̶ ̶t̶h̶i̶s̶ ̶s̶o̶l̶u̶t̶i̶o̶n̶ ̶i̶s̶ ̶p̶a̶r̶s̶i̶n̶g̶ ̶m̶u̶l̶t̶i̶p̶l̶e̶ ̶h̶e̶x̶a̶d̶e̶c̶i̶m̶a̶l̶s̶ ̶f̶r̶o̶m̶ ̶o̶n̶e̶ ̶s̶t̶r̶i̶n̶g̶,̶ ̶w̶h̶i̶c̶h̶ ̶r̶e̶q̶u̶i̶r̶e̶s̶ ̶a̶ ̶s̶e̶c̶o̶n̶d̶ ̶s̶t̶r̶e̶a̶m̶ ̶a̶n̶d̶ ̶a̶ ̶l̶o̶t̶ ̶o̶f̶ ̶a̶d̶d̶i̶t̶i̶o̶n̶a̶l̶ ̶l̶i̶n̶e̶s̶ ̶o̶f̶ ̶c̶o̶d̶e̶ ̶o̶f̶ ̶c̶l̶e̶a̶r̶i̶n̶g̶ ̶a̶n̶d̶ ̶g̶e̶t̶L̶i̶n̶e̶ ̶c̶a̶l̶l̶s̶.̶ After overloading the stream operators << and >> 对于我们的内部类型,一切看起来都很干净并且易于维护。解析多个十六进制也可以正常工作,我们只是在操作完成后将之前设置的std::hex值重置为std::dec。