这是我的问题。我尝试使用 boost::spirit::qi 并尝试使用“_1”和“_a”等占位符。我想访问 boost::qi/phoenix 占位符“后面”的底层对象,但我在这里有点挣扎。
假设我有以下课程:
class Tag {
public:
Tag() = default; // Needed by qi
Tag(std::uint8_t _raw_tag) : m_raw_tag( _raw_tag ) {}
std::uint8_t get_size() { return m_raw_tag & 0b111; }
std::uint8_t get_type() { return m_raw_tag & 0b1000; }
private:
std::uint8_t m_raw_tag;
};
我必须解析以标签字节开头的帧,该标签字节提供有关我接下来要阅读的内容的信息。为此,我编写了一个名为 Tag 的小帮助器类,它揭示了这些信息,例如标签的类型或接下来的数据的大小。我总是将数据存储在
std::uint32_t
中,但数据的大小可能是 3 个字节,而不是像 1、2 或 4 这样的预定义值,在这种情况下我可以分别使用 qi::byte
或 qi::big_word
或qi::big_qword
(假设采用大字节序)。因此,我正在考虑逐字节读取数据并在输出中对它们进行位移std::uint32_t
。
这将在伪 cpp 代码中给出这样的解析器:
template<typename _Iterator>
struct Read_frame : qi::grammar<_Iterator, std::uint32_t(), qi::locals<std::uint8_t>> {
Read_frame() : Read_frame::base_type(data_parser)
{
using boost::spirit::qi::byte_;
using boost::spirit::qi::omit;
using boost::spirit::qi::repeat;
using boost::spirit::qi::_val;
using namespace qi::labels;
tag_parser %= byte_;
// we read what's in the tag but we don't store it
// Call the method get_size() of Tag is my issue, I don't know how to do it
data_parser %= omit[tag_parser[ _a = _1.get_size()]] >> eps[_val = 0]
>> repeat(_a)[ byte_[ _val += (_1 << (--_a * 8)) ];
}
qi::rule<_Iterator, std::uint32_t(), qi::locals<std::uint8_t>> data_parser;
qi::rule<_Iterator, Tag()> tag_parser;
};
线路:
data_parser %= omit[context_tag[ _a = _1.get_size()]] >> eps[_val = 0]
这就是我的问题所在。我不知道如何在语义操作中访问标签的方法。因此我考虑使用
boost::phoenix::static_cast_<Tag*>(&_1)->get_size()
或类似的东西,但它不起作用。boost::spirit
与 boost::phoenix
一起使用,说实话,我认为我并不真正理解 boost
中的占位符如何工作,也不理解 boost::phoenix::static_cast_
的原理。这就是为什么我在这里温柔地请求你的帮助:)。如果您需要更多详细信息,我很乐意为您提供
提前致谢,
充满活力的新手
语义动作是懒惰的凤凰演员。也就是说,它们是“延迟函数”。您还可以将它们视为动态定义的组合函数。
“占位符背后的价值”取决于上下文。该上下文是运行时。 Phoenix 转换(“评估”)使用该上下文来检索占位符后面的实际对象在调用期间。
最后一部分是要点:任何运行时效果都必须推迟到调用期间。这意味着您需要一个 Phoenix actor 来访问
get_size()
方法并延迟调用它。
笨拙?你打赌。整个语义动作eDSL 是有限的。幸运的是,有很多方法可以解决这个问题:
您可以将
phoenix::bind
与指向成员函数的指针一起使用
您可以使用许多预定义的惰性函数来进行构造或大多数 STL (
#include <boost/phoenix/stl.hpp>
) 之类的事情。
顺便说一句。
phoenix::size
不适用于您的类型,因为它不遵守 STL 约定(size_t T::size() const
而不是
get_size
)。
您可以将自己的参与者编写为多态函数对象,并对其进行调整
phoenix::function<>
事实上,我最喜欢的做法已经成为
px::function f = [](auto& a, auto& b) { return a + b; };
,完全利用 C++17 CTAD
让我们演示所有或大部分这些。
正如我的评论中提到的,我对给定的解析器的明显行为有点困惑,所以让我们首先使用
phoenix::bind
方法作为示例来确定它:
template <typename It> struct Read_frame : qi::grammar<It, uint32_t(), qi::locals<uint8_t>> {
Read_frame() : Read_frame::base_type(data_parser) {
using namespace qi::labels;
tag_parser = qi::byte_;
auto _size = px::bind(&Tag::get_size, _1);
constexpr qi::_a_type _len;
data_parser //
= tag_parser[(_len = _size, _val = 0)] //
>> qi::repeat(_len)[ //
qi::byte_[_val += (_1 << --_len)] //
];
}
qi::rule<It, uint32_t(), qi::locals<uint8_t>> data_parser;
qi::rule<It, Tag()> tag_parser;
};
注意其他几个简化/可读性技巧。现在有一些测试用例Live On Compiler Explorer:
PASS [] -> none
PASS [0b00] -> optional(0)
PASS [0b01] -> none
PASS [0b01, 0b101010] -> optional(42)
PASS [0b10, 0b101010] -> none
PASS [0b10, 0b101010, 0b00] -> optional(84)
PASS [0b11, 0b101010, 0b00, 0b00] -> optional(168)
PASS [0b11111111] -> none
PASS [0b11111111, 0b01, 0b10, 0b11, 0b100, 0b101, 0b110, 0b111] -> optional(247)
我不会改变 qi::local,而是简单地逐步改变:
data_parser //
= tag_parser[(_len = _size, _val = 0)] //
>> qi::repeat(_len)[ //
qi::byte_[(_val <<= 1, _val += _1)] //
];
我们现在进行单元测试来验证行为是否相同:Live On Compiler Explorer。
正如所承诺的:
使用
phoenix::function
和 C++17 lambda 优点:Live
px::function get_size = [](Tag const& tag) { return tag.get_size(); };
data_parser //
= tag_parser[(_len = get_size(_1), _val = 0)] //
>> qi::repeat(_len)[ //
qi::byte_[(_val <<= 1, _val += _1)] //
];
请注意,延迟函数对象的本质是多态的,因此其工作原理是相同的:
px::function get_size = [](auto& tag) { return tag.get_size(); };
在没有 C++17 优点的情况下使用相同的内容:Live
template <typename It> struct Read_frame : qi::grammar<It, uint32_t(), qi::locals<uint8_t>> {
Read_frame() : Read_frame::base_type(data_parser) {
using namespace qi::labels;
constexpr qi::_a_type _len;
tag_parser = qi::byte_;
data_parser //
= tag_parser[(_len = get_size(_1), _val = 0)] //
>> qi::repeat(_len)[ //
qi::byte_[(_val <<= 1, _val += _1)] //
];
}
private:
struct get_size_f {
auto operator()(Tag const& tag) const { return tag.get_size(); };
};
px::function<get_size_f> get_size{};
qi::rule<It, uint32_t(), qi::locals<uint8_t>> data_parser;
qi::rule<It, Tag()> tag_parser;
};
使用适应宏(
BOOST_PHOENIX_ADAPT_CALLABLE
),Live
namespace {
struct get_size_f {
auto operator()(Tag const& tag) const { return tag.get_size(); };
};
BOOST_PHOENIX_ADAPT_CALLABLE(get_size_, get_size_f, 1);
} // namespace
template <typename It> struct Read_frame : qi::grammar<It, uint32_t(), qi::locals<uint8_t>> {
Read_frame() : Read_frame::base_type(data_parser) {
using namespace qi::labels;
constexpr qi::_a_type _len;
tag_parser = qi::byte_;
data_parser //
= tag_parser[(_len = get_size_(_1), _val = 0)] //
>> qi::repeat(_len)[ //
qi::byte_[(_val <<= 1, _val += _1)] //
];
}
private:
qi::rule<It, uint32_t(), qi::locals<uint8_t>> data_parser;
qi::rule<It, Tag()> tag_parser;
};
仍然使用 Qi,我会注意到
Tag
中没有任何内容需要将其用作属性类型。事实上,我们只需要简单的位掩码,如果您确实想要的话,它可能是一个免费函数。因此,这个最少的代码可以实现相同的功能,而不会带来太多不必要的复杂性:
#include <boost/phoenix.hpp>
#include <boost/spirit/include/qi.hpp>
namespace qi = boost::spirit::qi;
template <typename It> struct Read_frame : qi::grammar<It, uint32_t(), qi::locals<uint8_t>> {
Read_frame() : Read_frame::base_type(start) {
using namespace qi::labels;
start //
= qi::byte_[(_val = 0, _a = _1 & 0b111)] //
>> qi::repeat(_a)[ //
qi::byte_[(_val <<= 1, _val += _1)] //
];
}
private:
qi::rule<It, uint32_t(), qi::locals<uint8_t>> start;
};
免费功能也同样简单:Live
start //
= qi::byte_[(_val = 0, _a = px::bind(size_from_tag, _1))] //
>> qi::repeat(_a)[ //
qi::byte_[(_val <<= 1, _val += _1)] //
];
在现实生活中,我当然会编写一个自定义解析器。你可以在 Spirit Qi 中做到这一点,但为了与时俱进,大大减少编译时间,并且总体上让我的生活更轻松,我会选择 Spirit X3:
#include <boost/spirit/home/x3.hpp>
namespace Readers {
namespace x3 = boost::spirit::x3;
static constexpr uint8_t size_from_tag(uint8_t tag) { return tag & 0b111; }
struct frame_parser : x3::parser<frame_parser> {
using attribute_type = uint32_t;
bool parse(auto& first, auto last, auto&& /*ctx*/, auto&& /*rcontext*/, auto& attr) const {
if (first == last)
return false;
auto save = first;
uint8_t tag = *first++;
uint8_t len = size_from_tag(tag);
uint32_t val = 0;
while (len && first != last) {
--len;
val <<= 1;
val += static_cast<uint8_t>(*first++);
}
if (len == 0) {
attr = val;
return true;
}
first = save;
return false;
}
} static frame;
} // namespace Readers
#include <fmt/ranges.h>
#include <fmt/std.h>
int main() {
using Data = std::vector<uint8_t>;
struct {
Data input;
std::optional<uint32_t> expected;
} static const cases[]{
{{}, {}}, // empty input, expect nothing in return
{{0b0000}, 0},
{{0b0001}, {}}, // missing byte
{{0b0001, 42}, 42}, // 42
{{0b0010, 42}, {}}, // missing byte
{{0b0010, 42, 0}, 2 * 42}, // 2*42
{{0b0011, 42, 0, 0}, 4 * 42}, // 4*42
{{0xff}, {}}, // requires 7 bytes
{{0xff, 1, 2, 3, 4, 5, 6, 7}, 247}, // like this
};
for (auto& [data, expected] : cases) {
std::optional<uint32_t> actual;
auto ok = parse(begin(data), end(data), -Readers::frame, actual);
auto pass = (actual == expected);
auto verdict = pass ? "PASS" : "FAIL";
assert(ok); // optional parser should never fail, but we want to be sure
if (pass)
fmt::print("{} {::#04b} -> {}\n", verdict, data, actual);
else
fmt::print("{} {::#04b} -> {}\n\t *** expected: {}\n", verdict, data, actual, expected);
}
}
注意,这只会使编译速度加快 10 倍,我怀疑编译器会更容易优化。确实是这个程序
constexpr uint32_t parse_frame(auto const& input) {
uint8_t v;
parse(begin(input), end(input), x3::expect[Readers::frame], v);
return v;
}
int main() {
return parse_frame(std::array<uint8_t, 3>{0b0010, 42, 0}); // 2*42
}
全程优化
main:
mov eax, 84
ret
查看它Live On Compiler Explorer包括生成的汇编代码
¹ 通过手指浸渍证明