如何将 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 函数可能有点难以理解,但我也可以反转逻辑并逐边处理面(不确定我是否不过会用的)。