The signature
std::vector<uint8_t> EncodeU32(uint32_t);
implies a lot of copying, and temporary vector construction. Prefer something like
template <typename Out> Out EncodeLEB128(uint32_t, Out);
Here you use an output iterator instead of allocating vectors.
The seconds issue is that you need to know the DOM size, causing you to copy all of the DOM data again. Instead, make it so you can calculate the size up front and avoid the extra copy:
Let's also remove the code duplication that makes it hard to read/maintain the code.
Predicting Sizes: LEB128
Variable-length integer encoding is nice, but it complicates predicting the effective serialized data size. Let's create an extra helper:
template <typename T> size_t LEB128_Len(T d) {
return EncodeLEB128_Impl(std::move(d), [](auto&&){});
}
template <typename T, typename Out> Out EncodeLEB128(T d, Out out) {
EncodeLEB128_Impl(std::move(d), [&](uint8_t v) { *out++ = v; });
return out;
}
As you can see, I plan on implementing both with EncodeLEB128_Impl
- again avoiding code duplication. Here it is, you'll not it's pretty much identical to your original code, except for side-effects and genericity:
template <typename T, typename F> size_t EncodeLEB128_Impl(T d, F callback) {
static_assert(std::is_unsigned_v<T> && std::is_integral_v<T>);
// unsigned LEB128 encoding
size_t n = 0;
do {
unsigned int x = d & 0b01111111;
d >>= 7;
if (d)
x |= 0b10000000;
n++;
callback(x);
} while (d);
return n;
}
Predicting Content Length
Now we can move up to ranges. The length prediction can now become:
template <typename R>
size_t Range_Len(R const& range) {
using V = decltype(*std::begin(range));
size_t n = std::size(range);
return LEB128_Len(n) + n * sizeof(V);
}
That's ... nice! Now we can picture the end result:
std::vector<uint8_t> Serialize(DomType const& dom) {
auto const& name = dom.getName();
auto const& content = dom.getContent();
auto const domSize = Range_Len(name) + Range_Len(content);
std::vector<uint8_t> result(1 + LEB128_Len(domSize) + domSize);
auto out = result.begin();
*out++ = 0x00U; // dom ID
out = EncodeLEB128(domSize, out);
out = EncodeRange(name, out);
out = EncodeRange(content, out);
return result;
}
Notice how much cleaner that is! No more unnecessary copying or allocating, no more code duplication.
The only missing link is EncodeRange
:
template <std::contiguous_iterator It, typename Out>
Out EncodeRange(It f, It l, Out out) {
using V = typename std::iterator_traits<It>::value_type;
static_assert(std::is_trivially_copyable_v<V>);
size_t const n = std::distance(f, l);
auto const* bytes = reinterpret_cast<uint8_t const*>(std::addressof(*f));
return std::copy(bytes, bytes + n * sizeof(V), EncodeLEB128(n, out));
}
template <typename R, typename Out>
Out EncodeRange(R const& range, Out out) {
return EncodeRange(std::begin(range), std::end(range), out);
}
Live Demo
Live On Compiler Explorer
Live On Coliru
#include <cstdint>
#include <iterator>
#include <span>
#include <string_view>
#include <type_traits>
#include <vector>
struct DomType {
std::array<uint8_t, 16> data_{1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16};
std::span<uint8_t const> getContent() const { return data_; }
std::string_view getName() const { return "name"; }
};
template <typename T, typename F> size_t EncodeLEB128_Impl(T d, F callback) {
static_assert(std::is_unsigned_v<T> && std::is_integral_v<T>);
// unsigned LEB128 encoding
size_t n = 0;
do {
unsigned int x = d & 0b01111111;
d >>= 7;
if (d)
x |= 0b10000000;
n++;
callback(x);
} while (d);
return n;
}
template <typename T> size_t LEB128_Len(T d) {
return EncodeLEB128_Impl(std::move(d), [](auto&&){});
}
template <typename T, typename Out> Out EncodeLEB128(T d, Out out) {
EncodeLEB128_Impl(std::move(d), [&](uint8_t v) { *out++ = v; });
return out;
}
template <std::contiguous_iterator It, typename Out>
Out EncodeRange(It f, It l, Out out) {
using V = typename std::iterator_traits<It>::value_type;
static_assert(std::is_trivially_copyable_v<V>);
size_t const n = std::distance(f, l);
auto const* bytes = reinterpret_cast<uint8_t const*>(std::addressof(*f));
return std::copy(bytes, bytes + n * sizeof(V), EncodeLEB128(n, out));
}
template <typename R, typename Out>
Out EncodeRange(R const& range, Out out) {
return EncodeRange(std::begin(range), std::end(range), out);
}
template <typename R>
size_t Range_Len(R const& range) {
using V = decltype(*std::begin(range));
size_t n = std::size(range);
return LEB128_Len(n) + n * sizeof(V);
}
std::vector<uint8_t> Serialize(DomType const& dom) {
auto const& name = dom.getName();
auto const& content = dom.getContent();
auto const domSize = Range_Len(name) + Range_Len(content);
std::vector<uint8_t> result(1 + LEB128_Len(domSize) + domSize);
auto out = result.begin();
*out++ = 0x00U; // dom ID
out = EncodeLEB128(domSize, out);
out = EncodeRange(name, out);
out = EncodeRange(content, out);
return result;
}
#include <fmt/ranges.h>
int main() { fmt::print("Result: {::02x}", Serialize(DomType{})); }
Prints
Result: [00, 16, 04, 6e, 61, 6d, 65, 10, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 10]