如何将 scanf 精确匹配翻译成现代 c++ stringstream 读取
How to translate scanf exact matching into modern c++ stringstream reading
我目前正在做一个项目,我想使用现代 cpp 而不是依赖旧 c 来读取文件。
对于上下文,我正在尝试读取波前 obj 文件。
我有这个旧代码片段:
const char *line;
float x, y, z;
if(sscanf(line, "vn %f %f %f", &x, &y, &z) != 3)
break; // quitting loop because couldn't scan line correctly
我已将其翻译成:
string line;
string word;
stringstream ss(line);
float x, y, z;
if (!(ss >> word >> x >> y >> z)) // "vn x y z"
break; // quitting loop because couldn't scan line correctly
不同之处在于我使用 string
跳过第一个单词,但我希望它与 "vn"
匹配,就像 sscanf
一样。
这可能与 stringstream
还是我应该继续依赖 sscanf
进行精确的模式匹配?
我也在尝试翻译
sscanf(line, " %d/%d/%d", &i1, &i2, &i3);
但我遇到了困难,这再次使我倾向于不为我的文件使用现代 cpp reader。
我认为 stringstream
不是“现代 C++”¹。 (我也承认我们没有什么东西可以很好地替代 scanf
;我们确实有 std::format
,它非常好,并且用 std::cout <<
的语法让人联想到 Python 的格式字符串,而且速度更快。遗憾的是,它还没有达到标准库。)
我实际上会说,我认为您的 sscanf
代码比 stringstream 代码更干净、更健全:当解析行失败时,sscanf 使事情处于 easier-to-understand 状态。
您可以使用一些库来构建行解析器; Boost::spirit::qi
可能是最多的 well-known。你可以做这样的事情
auto success = (ss >> qi::phrase_match("vn " >> qi::double >> ' ' >> qi::double >> ' ' >> qi::double, x, y, z));
如果这个选项不会让你充满快乐、对世界的热爱和对all-reaching美的信仰,你和我一模一样 并发现它不能很好地替代原始 scanf
提供的紧凑性和易于理解性。
现在有 std::regex
库(自 C++11 起),这很可能就是您要找的东西!但是,您需要自己编写“这里有一个浮点数”的表达式,这也不是很酷。
¹ 不想伤害任何人,我认为 iostreams 库方法是一个单一的标准库功能,它使 C++ 比大多数现代语言更难使用 IO-related。这对代码质量有持久的影响。
我也 运行 了解此要求,并为流编写了一个小提取器,可让您匹配文字。代码如下所示:
#include <iostream>
#include <cctype>
std::istream& operator>>(std::istream& is, char const* s) {
if (s == nullptr)
return;
if (is.flags() & std::ios::skipws) {
while (std::isspace(is.peek()))
is.ignore(1);
while (std::isspace((unsigned char)* s))
++s;
}
while (*s && is.peek() == *s) {
is.ignore(1);
++s;
}
if (*s)
is.setstate(std::ios::failbit);
return is;
}
在你的情况下,你会像这样使用它:
if (!(ss >> "vn" >> x >> y >> z))
break;
正如您从代码中看到的那样,它会注意 skipws
状态,因此当且仅当您设置了 skipws 时,它才会跳过前导白色 space。因此,如果您需要匹配包含精确数量的前导 space 的模式,您需要关闭 skipws
,并将那些 space 包含在您的模式中。
对于这里感兴趣的任何人来说,我是如何用 stringstream
样式而不是 sscanf
样式解析我的 obj 文件的
旧代码:
for(;;)
{
if(fgets(line_buffer, sizeof(line_buffer), in) == NULL)
{
error= false; // eof
break;
}
// force endl
line_buffer[sizeof(line_buffer) -1]= 0;
// skip spaces
char *line= line_buffer;
while(*line && isspace(*line))
line++;
if(line[0] == 'v')
{
float x, y, z;
if(line[1] == ' ') // position x y z
{
if(sscanf(line, "v %f %f %f", &x, &y, &z) != 3)
break;
positions.push_back( vec3(x, y, z) );
}
else if(line[1] == 'n') // normal x y z
{
if(sscanf(line, "vn %f %f %f", &x, &y, &z) != 3)
break;
normals.push_back( vec3(x, y, z) );
}
else if(line[1] == 't') // texcoord x y
{
if(sscanf(line, "vt %f %f", &x, &y) != 2)
break;
texcoords.push_back( vec2(x, y) );
}
}
else if(line[0] == 'f')
{
idp.clear();
idt.clear();
idn.clear();
int next;
for(line= line +1; ; line= line + next)
{
idp.push_back(0);
idt.push_back(0);
idn.push_back(0); // 0: invalid index
next= 0;
if(sscanf(line, " %d/%d/%d %n", &idp.back(), &idt.back(), &idn.back(), &next) == 3)
continue;
else if(sscanf(line, " %d/%d %n", &idp.back(), &idt.back(), &next) == 2)
continue;
else if(sscanf(line, " %d//%d %n", &idp.back(), &idn.back(), &next) == 2)
continue;
else if(sscanf(line, " %d %n", &idp.back(), &next) == 1)
continue;
else if(next == 0) //endl
break;
}
}
新代码:(使用 std::istream& operator>>(std::istream& is, char const* s);
的@JerryCoffin 代码)
std::string line;
while (getline(file, line))
{
std::stringstream ss(line);
std::string tag;
// istream& operator>> skips whitespaces unless std::skipws is disable
// ignore empty lines
if (!(ss >> tag))
continue;
// ignore comments
if (tag[0] == '#')
continue;
float x, y, z;
if (tag == "v")
{
if (!(ss >> x >> y >> z))
break;
positions_tmp.emplace_back(x, y, z);
}
else if (tag == "vt")
{
if (!(ss >> x >> y))
break;
texcoords_tmp.emplace_back(x, y);
}
else if (tag == "vn")
{
if (!(ss >> x >> y >> z))
break;
normals_tmp.emplace_back(x, y, z);
}
else if (tag == "f")
{
for (int i = 0; i < 3; ++i)
{
unsigned int idp(0), idt(0), idn(0);
// reset stringstream after each read so that it keeps current position when fail
auto pos = ss.tellg();
auto state = ss.rdstate() && ~std::ios_base::failbit;
auto reset = [state, pos](std::stringstream& ss)
{ ss.clear(state); ss.seekg(pos); return !ss.fail(); };
// will try to match either i//i or i/i/i or i/i or i (all .obj "f" configurations)
if (reset(ss) && ss >> idp >> "//" >> idn) {}
else if (reset(ss) && ss >> idp >> "/" >> idt >> "/" >> idn) {}
else if (reset(ss) && ss >> idp >> "/" >> idt) {}
else if (reset(ss) && ss >> idp) {}
// do stuff with indexes
// ...
}
}
}
我在这里和那里更改了一些逻辑,但最棘手的部分是在 f i/i/i i/i/i i/i/i
行上,您必须检查它是否匹配正确。如果不是,则必须将 stringstream
重置为其原始状态和位置(至少删除 std::ios_base::failbit
并替换位置)。
我想保留一行的方面 = 一种情况,所以使用 lambda 函数可能有点难以理解,但我也可以反转逻辑并逐边处理面(不确定我是否不过会用的)。
我目前正在做一个项目,我想使用现代 cpp 而不是依赖旧 c 来读取文件。 对于上下文,我正在尝试读取波前 obj 文件。
我有这个旧代码片段:
const char *line;
float x, y, z;
if(sscanf(line, "vn %f %f %f", &x, &y, &z) != 3)
break; // quitting loop because couldn't scan line correctly
我已将其翻译成:
string line;
string word;
stringstream ss(line);
float x, y, z;
if (!(ss >> word >> x >> y >> z)) // "vn x y z"
break; // quitting loop because couldn't scan line correctly
不同之处在于我使用 string
跳过第一个单词,但我希望它与 "vn"
匹配,就像 sscanf
一样。
这可能与 stringstream
还是我应该继续依赖 sscanf
进行精确的模式匹配?
我也在尝试翻译
sscanf(line, " %d/%d/%d", &i1, &i2, &i3);
但我遇到了困难,这再次使我倾向于不为我的文件使用现代 cpp reader。
我认为 stringstream
不是“现代 C++”¹。 (我也承认我们没有什么东西可以很好地替代 scanf
;我们确实有 std::format
,它非常好,并且用 std::cout <<
的语法让人联想到 Python 的格式字符串,而且速度更快。遗憾的是,它还没有达到标准库。)
我实际上会说,我认为您的 sscanf
代码比 stringstream 代码更干净、更健全:当解析行失败时,sscanf 使事情处于 easier-to-understand 状态。
您可以使用一些库来构建行解析器; Boost::spirit::qi
可能是最多的 well-known。你可以做这样的事情
auto success = (ss >> qi::phrase_match("vn " >> qi::double >> ' ' >> qi::double >> ' ' >> qi::double, x, y, z));
如果这个选项不会让你充满快乐、对世界的热爱和对all-reaching美的信仰,你和我一模一样 并发现它不能很好地替代原始 scanf
提供的紧凑性和易于理解性。
现在有 std::regex
库(自 C++11 起),这很可能就是您要找的东西!但是,您需要自己编写“这里有一个浮点数”的表达式,这也不是很酷。
¹ 不想伤害任何人,我认为 iostreams 库方法是一个单一的标准库功能,它使 C++ 比大多数现代语言更难使用 IO-related。这对代码质量有持久的影响。
我也 运行 了解此要求,并为流编写了一个小提取器,可让您匹配文字。代码如下所示:
#include <iostream>
#include <cctype>
std::istream& operator>>(std::istream& is, char const* s) {
if (s == nullptr)
return;
if (is.flags() & std::ios::skipws) {
while (std::isspace(is.peek()))
is.ignore(1);
while (std::isspace((unsigned char)* s))
++s;
}
while (*s && is.peek() == *s) {
is.ignore(1);
++s;
}
if (*s)
is.setstate(std::ios::failbit);
return is;
}
在你的情况下,你会像这样使用它:
if (!(ss >> "vn" >> x >> y >> z))
break;
正如您从代码中看到的那样,它会注意 skipws
状态,因此当且仅当您设置了 skipws 时,它才会跳过前导白色 space。因此,如果您需要匹配包含精确数量的前导 space 的模式,您需要关闭 skipws
,并将那些 space 包含在您的模式中。
对于这里感兴趣的任何人来说,我是如何用 stringstream
样式而不是 sscanf
样式解析我的 obj 文件的
旧代码:
for(;;)
{
if(fgets(line_buffer, sizeof(line_buffer), in) == NULL)
{
error= false; // eof
break;
}
// force endl
line_buffer[sizeof(line_buffer) -1]= 0;
// skip spaces
char *line= line_buffer;
while(*line && isspace(*line))
line++;
if(line[0] == 'v')
{
float x, y, z;
if(line[1] == ' ') // position x y z
{
if(sscanf(line, "v %f %f %f", &x, &y, &z) != 3)
break;
positions.push_back( vec3(x, y, z) );
}
else if(line[1] == 'n') // normal x y z
{
if(sscanf(line, "vn %f %f %f", &x, &y, &z) != 3)
break;
normals.push_back( vec3(x, y, z) );
}
else if(line[1] == 't') // texcoord x y
{
if(sscanf(line, "vt %f %f", &x, &y) != 2)
break;
texcoords.push_back( vec2(x, y) );
}
}
else if(line[0] == 'f')
{
idp.clear();
idt.clear();
idn.clear();
int next;
for(line= line +1; ; line= line + next)
{
idp.push_back(0);
idt.push_back(0);
idn.push_back(0); // 0: invalid index
next= 0;
if(sscanf(line, " %d/%d/%d %n", &idp.back(), &idt.back(), &idn.back(), &next) == 3)
continue;
else if(sscanf(line, " %d/%d %n", &idp.back(), &idt.back(), &next) == 2)
continue;
else if(sscanf(line, " %d//%d %n", &idp.back(), &idn.back(), &next) == 2)
continue;
else if(sscanf(line, " %d %n", &idp.back(), &next) == 1)
continue;
else if(next == 0) //endl
break;
}
}
新代码:(使用 std::istream& operator>>(std::istream& is, char const* s);
的@JerryCoffin 代码)
std::string line;
while (getline(file, line))
{
std::stringstream ss(line);
std::string tag;
// istream& operator>> skips whitespaces unless std::skipws is disable
// ignore empty lines
if (!(ss >> tag))
continue;
// ignore comments
if (tag[0] == '#')
continue;
float x, y, z;
if (tag == "v")
{
if (!(ss >> x >> y >> z))
break;
positions_tmp.emplace_back(x, y, z);
}
else if (tag == "vt")
{
if (!(ss >> x >> y))
break;
texcoords_tmp.emplace_back(x, y);
}
else if (tag == "vn")
{
if (!(ss >> x >> y >> z))
break;
normals_tmp.emplace_back(x, y, z);
}
else if (tag == "f")
{
for (int i = 0; i < 3; ++i)
{
unsigned int idp(0), idt(0), idn(0);
// reset stringstream after each read so that it keeps current position when fail
auto pos = ss.tellg();
auto state = ss.rdstate() && ~std::ios_base::failbit;
auto reset = [state, pos](std::stringstream& ss)
{ ss.clear(state); ss.seekg(pos); return !ss.fail(); };
// will try to match either i//i or i/i/i or i/i or i (all .obj "f" configurations)
if (reset(ss) && ss >> idp >> "//" >> idn) {}
else if (reset(ss) && ss >> idp >> "/" >> idt >> "/" >> idn) {}
else if (reset(ss) && ss >> idp >> "/" >> idt) {}
else if (reset(ss) && ss >> idp) {}
// do stuff with indexes
// ...
}
}
}
我在这里和那里更改了一些逻辑,但最棘手的部分是在 f i/i/i i/i/i i/i/i
行上,您必须检查它是否匹配正确。如果不是,则必须将 stringstream
重置为其原始状态和位置(至少删除 std::ios_base::failbit
并替换位置)。
我想保留一行的方面 = 一种情况,所以使用 lambda 函数可能有点难以理解,但我也可以反转逻辑并逐边处理面(不确定我是否不过会用的)。