The recurring problem to assemble a network packet out of payload, sequence number, header, and other misc. information is mostly solved either on the heap (e.g. appending to a std::vector) or by first allocating a (hopefully large enough) buffer and then writing to that buffer. Some of the elements always stay the same or only change minimally (like the header) and therefore the scatter/gather approach offered by writev with iovec, Asio with buffer sequences or other networking interfaces allow to avoid those unnecessary copies.
There is still the problems that different parts of the message are produced in different parts of the code, especially when more than on sub-protocol is to be used. In that case we are again tempted to use dynamic memory allocation to construct the iovec.
I would like to avoid those dynamic memory allocation and potentially oversized buffers for so I came up with the following in-stack stack implementation (I named it stack_stack):
template<class T, size_t length=1>
struct stack_stack {
using next_type = stack_stack<T, length-1>;
using prev_type = stack_stack<T, length+1>;
const T value;
const next_type * next = nullptr;
static constexpr size_t ssize = length;
struct iterator {
using value_type = T;
using pointer = const value_type*;
using reference = const value_type&;
using iterator_category = std::input_iterator_tag;
iterator& operator++() {
ptr = static_cast<const stack_stack*>(ptr)->next;
return *this;
}
bool operator==(iterator other) const {
return ptr == *other;
}
bool operator!=(iterator other) const {
return ptr != *other;
}
pointer operator*() {return static_cast<pointer>(ptr);}
const void* ptr;
};
iterator begin() const {return iterator{this};}
iterator end() const {return iterator{nullptr};}
prev_type push_front(T val) const {
return {val, this};
}
};
It keeps track of its length using template parameters and could be used like in the following example scenario:
struct ioitem {
char* data;
size_t size;
};
template<class stack>
void Send(const stack& b) {
for (auto a : b) {
std::cout << a->data << std::endl;
}
}
template<class stack>
void SendWithHeader(const stack& b) {
auto header = std::string("HDX1"); // This would normally some kind of constexpr
Send(b.push_front({header.data(), header.size()}));
}
template<class stack>
void SendWithSeqno(const stack& b) {
auto seq_no = std::string("5");
auto b1 = b.push_front({seq_no.data(), seq_no.size()}); // it's ok if one module addds more than one part
auto b2 = b1.push_front({seq_no.data(), seq_no.size()});
SendWithHeader(b2);
}
template<class stack>
void SendWithTag(const stack& b) {
auto tag_name = std::string("my tag"); // I am just making up a protocol here
SendWithSeqno(b.push_front({tag_name.data(), tag_name.size()}));
}
int main() {
auto my_data = std::string("Hello World");
auto my_Buffer = stack_stack<ioitem>{my_data.data(), my_data.size()};
SendWithTag(my_Buffer);
}
What I would like to improve:
- In the
Sendfunction I could copy the stack to a statically sized array according to the size ofstack::ssize. However I did not getstd::copyto work. - I don't like the hacks with the
void*in the iterator.
Also: Is this a good way to approach this problem or is there a much better solution (without the heap) that got under my radar? I searched for similar implementations to mine but could not find anything.