处理INI文件中重复的节名 我需要从INI文件中加载这些值,并使用C++ Boost库在应用程序中打印它们。这些部分有重复的名称。我仅限于使用C++ Boost库。 numColors = 4 boardSize = 11 numSnails = 2 [initialization] id = 0 row = 3 col = 4 orientation = 0 [initialization] id = 1 row = 5 col = 0 orientation = 1 [color] id = 0 nextColor = 1 deltaOrientation = +2 [color] id = 1 nextColor = 2 deltaOrientation = +1 [color] id = 2 nextColor = 3 deltaOrientation = -2 [color] id = 3 nextColor = 0 deltaOrientation = -1 它不是什么

处理INI文件中重复的节名 我需要从INI文件中加载这些值,并使用C++ Boost库在应用程序中打印它们。这些部分有重复的名称。我仅限于使用C++ Boost库。 numColors = 4 boardSize = 11 numSnails = 2 [initialization] id = 0 row = 3 col = 4 orientation = 0 [initialization] id = 1 row = 5 col = 0 orientation = 1 [color] id = 0 nextColor = 1 deltaOrientation = +2 [color] id = 1 nextColor = 2 deltaOrientation = +1 [color] id = 2 nextColor = 3 deltaOrientation = -2 [color] id = 3 nextColor = 0 deltaOrientation = -1 它不是什么,boost,ini,Boost,Ini,简而言之,这根本不是INI格式。它只是非常松散地像它。这很好 它是什么? 你没有详细说明,所以我要做一些假设 为了简单起见,我假设 初始化部分位于颜色部分之前 类似部分中的键始终具有相同的顺序 在类似部分中,所示的所有键都是必需的 增量是有符号整数值(正号是可选的) 所有其他值都是非负整数 空白并不重要 这起案件意义重大 所有数字均为十进制形式(不考虑前导零) 非必要扣减(可用于添加更多验证): 初始化次数=numSnails 线路板尺寸指示行和列的大小为[0,线路板尺寸) 数据结构 为

简而言之,这根本不是INI格式。它只是非常松散地像它。这很好

它是什么? 你没有详细说明,所以我要做一些假设

为了简单起见,我假设

  • 初始化部分位于颜色部分之前
  • 类似部分中的键始终具有相同的顺序
  • 在类似部分中,所示的所有键都是必需的
  • 增量是有符号整数值(正号是可选的)
  • 所有其他值都是非负整数
  • 空白并不重要
  • 这起案件意义重大
  • 所有数字均为十进制形式(不考虑前导零)
非必要扣减(可用于添加更多验证):

  • 初始化次数=numSnails
  • 线路板尺寸指示行和列的大小为[0,线路板尺寸)
数据结构 为了表示该文件,我将:

namespace Ast {
    struct Initialization {
        unsigned id, row, col, orientation;
    };

    struct Color {
        unsigned id, nextColor;
        int deltaOrientation;
    };

    struct File {
        unsigned numColors, boardSize, numSnails;

        std::vector<Initialization> initializations;
        std::vector<Color>          colors;
    };
}
我们基本上可以让解析器“自行编写”:

另请参见下面的奖励部分,该部分将大大改进这一点

测试/现场演示 通过测试,我们将首先读取文件,然后使用该解析器对其进行解析:

std::string read_file(std::string name) {
    std::ifstream ifs(name);
    return std::string(std::istreambuf_iterator<char>(ifs), {});
}

static Ast::File parse_game(std::string_view input) {
    using SVI = std::string_view::const_iterator;
    static const GameParser<SVI> parser{};

    try {
        Ast::File parsed;
        if (qi::parse(input.begin(), input.end(), parser, parsed)) {
            return parsed;
        }
        throw std::runtime_error("Unable to parse game");
    } catch (qi::expectation_failure<SVI> const& ef) {
        std::ostringstream oss;
        oss << "Expected: " << ef.what_;
        throw std::runtime_error(oss.str());
    }
}
缺乏产出意味着成功

奖金 一些改进,而不是使用
auto.
为类型生成正确的解析器,我们可以使其显式:

namespace Ast {
    using Id          = unsigned;
    using Size        = uint8_t;
    using Coord       = Size;
    using ColorNumber = Size;
    using Orientation = Size;
    using Delta       = signed;

    struct Initialization {
        Id          id;
        Coord       row;
        Coord       col;
        Orientation orientation;
    };

    struct Color {
        Id          id;
        ColorNumber nextColor;
        Delta       deltaOrientation;
    };

    struct File {
        Size numColors{}, boardSize{}, numSnails{};

        std::vector<Initialization> initializations;
        std::vector<Color>          colors;
    };
}  // namespace Ast
现在我们可以改进错误消息,例如:

input.txt:2:13 Expected: <unsigned-integer>
 note: boardSize = (11)
 note:             ^--- here
input.txt:2:13预期值:
注:boardSize=(11)
注:^---

input.txt:16:19预期值:

/#定义BOOST_SPIRIT_调试
#包括
#包括
#包括
#包括
名称空间qi=boost::spirit::qi;
名称空间Ast{
使用Id=无符号;
使用大小=uint8\u t;
使用坐标=大小;
使用ColorNumber=大小;
使用方向=大小;
使用Delta=signed;
结构初始化{
身份证;
库德街;
库德山口;
定位;
};
结构颜色{
身份证;
颜色数下一个颜色;
三角洲三角洲定向;
};
结构文件{
大小numColors{},boardSize{},numSnails{};
std::向量初始化;
矢量颜色;
};
}//名称空间Ast
BOOST\u FUSION\u ADAPT\u STRUCT(Ast::初始化、id、行、列、方向)
BOOST\u FUSION\u ADAPT\u STRUCT(Ast::Color、id、nextColor、deltaOrientation)
BOOST\u FUSION\u ADAPT\u STRUCT(Ast::File、numColors、boardSize、numSnails、,
初始化、颜色)
模板
结构语法分析器:qi::grammar{
GameParser():GameParser::基本类型(开始){
使用名称空间qi;
开始=跳过(空白)[文件];
自动段=[](常量标准::字符串和名称){
返回副本('['>>lexeme[lit(name)]>']'>>(+eol | eoi));
};
自动要求=[](常量标准::字符串和名称,自动值){
返回副本(lexeme[eps>lit(name)]>'='>value>
(+eol | eoi));
};
文件=
所需(“numColors”,尺寸)>
所需(“板尺寸”、\u尺寸)>
所需(“numSnails”,尺寸)>
*初始化>
*颜色>
eoi;//必须到达输入的末尾
初始化=节(“初始化”)>
必需(“id”、\U id)>
所需(“行”,协调)>
必需(“col”、_coord)>
必需(“方向”、_方向);
颜色=部分(“颜色”)>
必需(“id”、\U id)>
必填项(“nextColor”、\U COLORNAMER)>
所需(“三角洲定位”);
BOOST_SPIRIT_DEBUG_节点((文件)(初始化)(颜色))
}
私人:
使用Skipper=qi::blank\u类型;
qi::规则开始;
qi::规则文件;
qi::规则初始化;
规则颜色;

qi::uint_解析器

我想,对于一个随机的路人来说,可能看起来很复杂/过分

我想再试一次,因为现在是2021年,无论是使用C++17,我们都可以用标准库完成合理的工作

事实证明,这需要更多的工作。Qi实现需要86行代码,而标准库实现需要136行代码。此外,调试/编写花费了我更长的时间(几个小时)。特别是,很难获得
'='
,'
['
,'
]“
作为带有
std::istream&
的令牌边界。我使用了回答中的
ctype
facet方法:

我确实在调试程序中留下了(20行),这样你也许可以自己理解它

短期博览会 顶级解析函数看起来很正常,并显示了我想要实现的功能:natural
std::istream
extraction:

static Ast::File std_parse_game(std::string_view input) {
    std::istringstream iss{std::string(input)};

    using namespace Helpers;
    if (Ast::File parsed; iss >> parsed)
        return parsed;
    throw std::runtime_error("Unable to parse game");
}
其余的都位于命名空间
帮助程序中

static inline std::istream& operator>>(std::istream& is, Ast::File& v) {

    for (section s; is >> s;) {
        if (s.name == "parameters")
            is >> v.parameters;
        else if (s.name == "initialization")
            is >> v.initializations.emplace_back();
        else if (s.name == "color")
            is >> v.colors.emplace_back();
        else
            is.setstate(std::ios::failbit);
    }
    if (is.eof())
        is.clear();
    return is;
}
到目前为止,效果良好。不同的路段类型相似:

static inline std::istream& operator>>(std::istream& is, Ast::Parameters& v) {
    return is
        >> entry{"numColors", v.numColors}
        >> entry{"boardSize", v.boardSize}
        >> entry{"numSnails", v.numSnails};
}

static inline std::istream& operator>>(std::istream& is, Ast::Initialization& v) {
    return is
        >> entry{"id", v.id}
        >> entry{"row", v.row}
        >> entry{"col", v.col}
        >> entry{"orientation", v.orientation};
}

static inline std::istream& operator>>(std::istream& is, Ast::Color& v) {
    return is
        >> entry{"id", v.id}
        >> entry{"nextColor", v.nextColor}
        >> entry{"deltaOrientation", v.deltaOrientation};
}
现在,如果一切都像这样一帆风顺,我就不会推荐勇气。现在我们进入有条件的帕辛。
条目{“name”,value}
公式使用“操纵器类型”:

条件对于在不将流置于硬失败模式的情况下检测EOF非常重要(
is.bad()
!=
is.fail()

expect
构建在
令牌
之上:

struct section {
    std::string name;
    friend std::istream& operator>>(std::istream& is, section& s) {
        if (is >> expect('['))
            return is >> token{s.name} >> expect{']'};
        return is;
    }
};
template <typename T> struct expect {
    expect(T expected) : _expected(expected) {}
    T _expected;
    friend std::istream& operator>>(std::istream& is, expect const& e) {
        if (T actual; is >> token{actual})
            if (actual != e._expected)
                is.setstate(std::ios::failbit);
        return is;
    }
};
然后我们需要使用它,但只有当我们解析一个
std::string
标记时才需要使用它 另外,如果我们
expect('=')
它将不会跳过
'='
,因为我们的方面将其称为空白

template <typename T> struct token {
    token(T& into) : _into(into) {}
    T& _into;
    friend std::istream& operator>>(std::istream& is, token const& t) {
        std::locale loc = is.getloc();
        if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
            loc = is.imbue(std::locale(std::locale(), new mytoken_ctype()));
        }

        try { is >> t._into; is.imbue(loc); }
        catch (...) { is.imbue(loc); throw; }
        return is;
    }
};
让我松了一口气的是,解析器在数据上达成了一致:

Qi:    ((4 11 2)
        vector<Ast::Initialization>{(0 3 4 0), (1 5 0 1)}
        vector<Ast::Color>{(0 1 2), (1 2 1), (2 3 -2), (3 0 -1)})
std:   ((4 11 2)
        vector<Ast::Initialization>{(0 3 4 0), (1 5 0 1)}
        vector<Ast::Color>{(0 1 2), (1 2 1), (2 3 -2), (3 0 -1)})
Equal: true
如果您的输入格式不稳定,并且是计算机编写的,我强烈建议您不要使用该标准
//#define BOOST_SPIRIT_DEBUG
#include <boost/spirit/home/qi.hpp>
#include <fstream>
#include <sstream>
#include <iomanip>
namespace qi = boost::spirit::qi;

namespace Ast {
    using Id          = unsigned;
    using Size        = uint8_t;
    using Coord       = Size;
    using ColorNumber = Size;
    using Orientation = Size;
    using Delta       = signed;

    struct Initialization {
        Id          id;
        Coord       row;
        Coord       col;
        Orientation orientation;
    };

    struct Color {
        Id          id;
        ColorNumber nextColor;
        Delta       deltaOrientation;
    };

    struct File {
        Size numColors{}, boardSize{}, numSnails{};

        std::vector<Initialization> initializations;
        std::vector<Color>          colors;
    };
}  // namespace Ast

BOOST_FUSION_ADAPT_STRUCT(Ast::Initialization, id, row, col, orientation)
BOOST_FUSION_ADAPT_STRUCT(Ast::Color, id, nextColor, deltaOrientation)
BOOST_FUSION_ADAPT_STRUCT(Ast::File, numColors, boardSize, numSnails,
                          initializations, colors)

template <typename It>
struct GameParser : qi::grammar<It, Ast::File()> {
    GameParser() : GameParser::base_type(start) {
        using namespace qi;
        start = skip(blank)[file];

        auto section = [](const std::string& name) {
            return copy('[' >> lexeme[lit(name)] >> ']' >> (+eol | eoi));
        };
        auto required = [](const std::string& name, auto value) {
            return copy(lexeme[eps > lit(name)] > '=' > value >
                        (+eol | eoi));
        };

        file =
            required("numColors", _size) >
            required("boardSize", _size) >
            required("numSnails", _size) >
            *initialization >
            *color >
            eoi; // must reach end of input

        initialization = section("initialization") >
            required("id", _id) >
            required("row", _coord) >
            required("col", _coord) >
            required("orientation", _orientation);
            
        color = section("color") >
            required("id", _id) >
            required("nextColor", _colorNumber) >
            required("deltaOrientation", _delta);

        BOOST_SPIRIT_DEBUG_NODES((file)(initialization)(color))
    }

  private:
    using Skipper = qi::blank_type;
    qi::rule<It, Ast::File()>                    start;
    qi::rule<It, Ast::File(), Skipper>           file;
    qi::rule<It, Ast::Initialization(), Skipper> initialization;
    qi::rule<It, Ast::Color(), Skipper>          color;

    qi::uint_parser<Ast::Id>          _id;
    qi::uint_parser<Ast::Size>        _size;
    qi::uint_parser<Ast::Coord>       _coord;
    qi::uint_parser<Ast::ColorNumber> _colorNumber;
    qi::uint_parser<Ast::Orientation> _orientation;
    qi::int_parser<Ast::Delta>        _delta;
};

std::string read_file(const std::string& name) {
    std::ifstream ifs(name);
    return std::string(std::istreambuf_iterator<char>(ifs), {});
}

static Ast::File parse_game(std::string_view input) {
    using SVI = std::string_view::const_iterator;
    static const GameParser<SVI> parser{};

    try {
        Ast::File parsed;
        if (qi::parse(input.begin(), input.end(), parser, parsed)) {
            return parsed;
        }
        throw std::runtime_error("Unable to parse game");
    } catch (qi::expectation_failure<SVI> const& ef) {
        std::ostringstream oss;

        auto where  = ef.first - input.begin();
        auto sol    = 1 + input.find_last_of("\r\n", where);
        auto lineno = 1 + std::count(input.begin(), input.begin() + sol, '\n');
        auto col    = 1 + where - sol;
        auto llen   = input.substr(sol).find_first_of("\r\n");

        oss << "input.txt:" << lineno << ":" << col << " Expected: " << ef.what_ << "\n"
            << " note: " << input.substr(sol, llen) << "\n"
            << " note:"  << std::setw(col) << "" << "^--- here";
        throw std::runtime_error(oss.str());
    }
}

int main() {
    std::string game_save = read_file("input.txt");

    try {
        Ast::File data = parse_game(game_save);
    } catch (std::exception const& e) {
        std::cerr << e.what() << "\n";
    }
}
static Ast::File std_parse_game(std::string_view input) {
    std::istringstream iss{std::string(input)};

    using namespace Helpers;
    if (Ast::File parsed; iss >> parsed)
        return parsed;
    throw std::runtime_error("Unable to parse game");
}
static inline std::istream& operator>>(std::istream& is, Ast::File& v) {

    for (section s; is >> s;) {
        if (s.name == "parameters")
            is >> v.parameters;
        else if (s.name == "initialization")
            is >> v.initializations.emplace_back();
        else if (s.name == "color")
            is >> v.colors.emplace_back();
        else
            is.setstate(std::ios::failbit);
    }
    if (is.eof())
        is.clear();
    return is;
}
static inline std::istream& operator>>(std::istream& is, Ast::Parameters& v) {
    return is
        >> entry{"numColors", v.numColors}
        >> entry{"boardSize", v.boardSize}
        >> entry{"numSnails", v.numSnails};
}

static inline std::istream& operator>>(std::istream& is, Ast::Initialization& v) {
    return is
        >> entry{"id", v.id}
        >> entry{"row", v.row}
        >> entry{"col", v.col}
        >> entry{"orientation", v.orientation};
}

static inline std::istream& operator>>(std::istream& is, Ast::Color& v) {
    return is
        >> entry{"id", v.id}
        >> entry{"nextColor", v.nextColor}
        >> entry{"deltaOrientation", v.deltaOrientation};
}
template <typename T> struct entry {
    entry(std::string name, T& into) : _name(name), _into(into) {}
    std::string _name;
    T& _into;
    friend std::istream& operator>>(std::istream& is, entry e) {
        return is >> expect{e._name} >> expect{'='} >> e._into;
    }
};
struct section {
    std::string name;
    friend std::istream& operator>>(std::istream& is, section& s) {
        if (is >> expect('['))
            return is >> token{s.name} >> expect{']'};
        return is;
    }
};
template <typename T> struct expect {
    expect(T expected) : _expected(expected) {}
    T _expected;
    friend std::istream& operator>>(std::istream& is, expect const& e) {
        if (T actual; is >> token{actual})
            if (actual != e._expected)
                is.setstate(std::ios::failbit);
        return is;
    }
};
// make sure =,[,] break tokens
struct mytoken_ctype : std::ctype<char> {
    static auto const* get_table() {
        static std::vector rc(table_size, std::ctype_base::mask());

        rc[' '] = rc['\f'] = rc['\v'] = rc['\t'] = rc['\r'] = rc['\n'] =
            std::ctype_base::space;
        // crucial for us:
        rc['='] = rc['['] = rc[']'] = std::ctype_base::space;
        return rc.data();
    }

    mytoken_ctype() : std::ctype<char>(get_table()) {}
};
template <typename T> struct token {
    token(T& into) : _into(into) {}
    T& _into;
    friend std::istream& operator>>(std::istream& is, token const& t) {
        std::locale loc = is.getloc();
        if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
            loc = is.imbue(std::locale(std::locale(), new mytoken_ctype()));
        }

        try { is >> t._into; is.imbue(loc); }
        catch (...) { is.imbue(loc); throw; }
        return is;
    }
};
#include <boost/spirit/home/qi.hpp>
#include <boost/fusion/include/io.hpp>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
namespace qi = boost::spirit::qi;

namespace Ast {
    using Id          = unsigned;
    using Size        = uint16_t; // avoiding char types for easy debug/output
    using Coord       = Size;
    using ColorNumber = Size;
    using Orientation = Size;
    using Delta       = signed;

    struct Parameters {
        Size numColors{}, boardSize{}, numSnails{};

        bool operator==(Parameters const&) const = default;
    };

    struct Initialization {
        Id          id;
        Coord       row;
        Coord       col;
        Orientation orientation;

        bool operator==(Initialization const&) const = default;
    };

    struct Color {
        Id          id;
        ColorNumber nextColor;
        Delta       deltaOrientation;

        bool operator==(Color const&) const = default;
    };

    struct File {
        Parameters                  parameters;
        std::vector<Initialization> initializations;
        std::vector<Color>          colors;

        bool operator==(File const&) const = default;
    };

    using boost::fusion::operator<<;

    template <typename T>
    static inline std::ostream& operator<<(std::ostream& os, std::vector<T> const& v) {
        return os << fmt::format("vector<{}>{}",
                                 boost::core::demangle(typeid(T).name()), v);
    }
}  // namespace Ast

BOOST_FUSION_ADAPT_STRUCT(Ast::Parameters, numColors, boardSize, numSnails)
BOOST_FUSION_ADAPT_STRUCT(Ast::Initialization, id, row, col, orientation)
BOOST_FUSION_ADAPT_STRUCT(Ast::Color, id, nextColor, deltaOrientation)
BOOST_FUSION_ADAPT_STRUCT(Ast::File, parameters, initializations, colors)

template <typename It>
struct GameParser : qi::grammar<It, Ast::File()> {
    GameParser() : GameParser::base_type(start) {
        using namespace qi;
        start = skip(blank)[file];

        auto section = [](const std::string& name) {
            return copy('[' >> lexeme[lit(name)] >> ']' >> (+eol | eoi));
        };
        auto required = [](const std::string& name, auto value) {
            return copy(lexeme[eps > lit(name)] > '=' > value >
                        (+eol | eoi));
        };

        file = parameters >
            *initialization >
            *color >
            eoi; // must reach end of input

        parameters = section("parameters") >
            required("numColors", _size) >
            required("boardSize", _size) >
            required("numSnails", _size);

        initialization = section("initialization") >
            required("id", _id) >
            required("row", _coord) >
            required("col", _coord) >
            required("orientation", _orientation);
            
        color = section("color") >
            required("id", _id) >
            required("nextColor", _colorNumber) >
            required("deltaOrientation", _delta);

        BOOST_SPIRIT_DEBUG_NODES((file)(parameters)(initialization)(color))
    }

  private:
    using Skipper = qi::blank_type;
    qi::rule<It, Ast::File()>                    start;
    qi::rule<It, Ast::File(), Skipper>           file;
    // sections
    qi::rule<It, Ast::Parameters(), Skipper>     parameters;
    qi::rule<It, Ast::Initialization(), Skipper> initialization;
    qi::rule<It, Ast::Color(), Skipper>          color;

    // value types
    qi::uint_parser<Ast::Id>          _id;
    qi::uint_parser<Ast::Size>        _size;
    qi::uint_parser<Ast::Coord>       _coord;
    qi::uint_parser<Ast::ColorNumber> _colorNumber;
    qi::uint_parser<Ast::Orientation> _orientation;
    qi::int_parser<Ast::Delta>        _delta;
};

static Ast::File qi_parse_game(std::string_view input) {
    using SVI = std::string_view::const_iterator;
    static const GameParser<SVI> parser{};

    try {
        Ast::File parsed;
        if (qi::parse(input.begin(), input.end(), parser, parsed)) {
            return parsed;
        }
        throw std::runtime_error("Unable to parse game");
    } catch (qi::expectation_failure<SVI> const& ef) {
        std::ostringstream oss;

        auto where  = ef.first - input.begin();
        auto sol    = 1 + input.find_last_of("\r\n", where);
        auto lineno = 1 + std::count(input.begin(), input.begin() + sol, '\n');
        auto col    = 1 + where - sol;
        auto llen   = input.substr(sol).find_first_of("\r\n");

        oss << "input.txt:" << lineno << ":" << col << " Expected: " << ef.what_ << "\n"
            << " note: " << input.substr(sol, llen) << "\n"
            << " note:"  << std::setw(col) << "" << "^--- here";
        throw std::runtime_error(oss.str());
    }
}

namespace Helpers {
    struct DebugPeeker {
        DebugPeeker(std::istream& is, int line) : is(is), line(line) { dopeek(); }
        ~DebugPeeker() { dopeek(); }

      private:
        std::istream& is;
        int line;

        void dopeek() const {
            std::char_traits<char> t;
            auto ch = is.peek();
            std::cerr << "DEBUG " << line << " Peek: ";
            if (std::isgraph(ch))
                std::cerr << "'" << t.to_char_type(ch) << "'";
            else 
                std::cerr << "<" << ch << ">";
            std::cerr << " " << std::boolalpha << is.good() << "\n";
        }
    };

#define DEBUG_PEEK(is) // Peeker _peek##__LINE__(is, __LINE__);

    // make sure =,[,] break tokens
    struct mytoken_ctype : std::ctype<char> {
        static auto const* get_table() {
            static std::vector rc(table_size, std::ctype_base::mask());

            rc[' '] = rc['\f'] = rc['\v'] = rc['\t'] = rc['\r'] = rc['\n'] =
                std::ctype_base::space;
            // crucial for us:
            rc['='] = rc['['] = rc[']'] = std::ctype_base::space;
            return rc.data();
        }

        mytoken_ctype() : std::ctype<char>(get_table()) {}
    };

    template <typename T> struct token {
        token(T& into) : _into(into) {}
        T& _into;
        friend std::istream& operator>>(std::istream& is, token const& t) {
            DEBUG_PEEK(is);
            std::locale loc = is.getloc();
            if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
                loc = is.imbue(std::locale(std::locale(), new mytoken_ctype()));
            }

            try { is >> t._into; is.imbue(loc); }
            catch (...) { is.imbue(loc); throw; }
            return is;
        }
    };

    template <typename T> struct expect {
        expect(T expected) : _expected(expected) {}
        T _expected;
        friend std::istream& operator>>(std::istream& is, expect const& e) {
            DEBUG_PEEK(is);
            if (T actual; is >> token{actual})
                if (actual != e._expected)
                    is.setstate(std::ios::failbit);
            return is;
        }
    };

    template <typename T> struct entry {
        entry(std::string name, T& into) : _name(name), _into(into) {}
        std::string _name;
        T& _into;
        friend std::istream& operator>>(std::istream& is, entry e) {
            DEBUG_PEEK(is);
            return is >> expect{e._name} >> expect{'='} >> e._into;
        }
    };

    struct section {
        std::string name;
        friend std::istream& operator>>(std::istream& is, section& s) {
            DEBUG_PEEK(is);
            if (is >> expect('['))
                return is >> token{s.name} >> expect{']'};
            return is;
        }
    };

    static inline std::istream& operator>>(std::istream& is, Ast::Parameters& v) {
        DEBUG_PEEK(is);
        return is
            >> entry{"numColors", v.numColors}
            >> entry{"boardSize", v.boardSize}
            >> entry{"numSnails", v.numSnails};
    }

    static inline std::istream& operator>>(std::istream& is, Ast::Initialization& v) {
        DEBUG_PEEK(is);
        return is
            >> entry{"id", v.id}
            >> entry{"row", v.row}
            >> entry{"col", v.col}
            >> entry{"orientation", v.orientation};
    }

    static inline std::istream& operator>>(std::istream& is, Ast::Color& v) {
        DEBUG_PEEK(is);
        return is
            >> entry{"id", v.id}
            >> entry{"nextColor", v.nextColor}
            >> entry{"deltaOrientation", v.deltaOrientation};
    }

    static inline std::istream& operator>>(std::istream& is, Ast::File& v) {
        DEBUG_PEEK(is);

        for (section s; is >> s;) {
            if (s.name == "parameters")
                is >> v.parameters;
            else if (s.name == "initialization")
                is >> v.initializations.emplace_back();
            else if (s.name == "color")
                is >> v.colors.emplace_back();
            else
                is.setstate(std::ios::failbit);
        }
        if (is.eof())
            is.clear();
        return is;
    }
}

static Ast::File std_parse_game(std::string_view input) {
    std::istringstream iss{std::string(input)};

    using namespace Helpers;
    if (Ast::File parsed; iss >> parsed)
        return parsed;
    throw std::runtime_error("Unable to parse game");
}

std::string read_file(const std::string& name) {
    std::ifstream ifs(name);
    return std::string(std::istreambuf_iterator<char>(ifs), {});
}

int main() {
    std::string const game_save = read_file("input.txt");

    Ast::File g1, g2;
    try {
        std::cout << "Qi:    " << (g1 = qi_parse_game(game_save)) << "\n";
    } catch (std::exception const& e) { std::cerr << e.what() << "\n"; }
    try {
        std::cout << "std:   " << (g2 = std_parse_game(game_save)) << "\n";
    } catch (std::exception const& e) { std::cerr << e.what() << "\n"; }

    std::cout << "Equal: " << std::boolalpha << (g1 == g2) << "\n";
}
Qi:    ((4 11 2)
        vector<Ast::Initialization>{(0 3 4 0), (1 5 0 1)}
        vector<Ast::Color>{(0 1 2), (1 2 1), (2 3 -2), (3 0 -1)})
std:   ((4 11 2)
        vector<Ast::Initialization>{(0 3 4 0), (1 5 0 1)}
        vector<Ast::Color>{(0 1 2), (1 2 1), (2 3 -2), (3 0 -1)})
Equal: true
[parameters] numColors=999 boardSize=999 numSnails=999
[color] id=0 nextColor=1 deltaOrientation=+2 [color] id=1 nextColor=2
                         deltaOrientation=+1 [
initialization] id=1 row=5 col=0 orientation=1
[color] id=2 nextColor=3 deltaOrientation=-2
[parameters] numColors=4 boardSize=11 numSnails=2
[color] id=3 nextColor=0 deltaOrientation=-1
[initialization] id=0 row=3 col=4 orientation=0