1

I'd like a way to take an arbitrary-size array of bytes and return a hex string. Specifically for logging packets sent over the net, but I use the equivalent function that takes a std::vector a lot. Something like this, probably a template?

std::string hex_str(const std::array<uint8_t,???> array);

I've searched but the solutions all say "treat it as a C-style array" and I am specifically asking whether there's a way not to do that. I assume the reason this isn't in every single C++ FAQ is that it's impossible and if so can someone outline why?

I already have these overloads and the second one can be used for std::array by decaying into a C-style array, so please don't tell me how to do that.

std::string hex_str(const std::vector<uint8_t> &data);
std::string hex_str(const uint8_t *data, const size_t size);

(edit: vector is a reference in my code)

6
  • 1
    What do you mean hex string? Base64 encoded or what? Commented Oct 14, 2021 at 0:39
  • 1
    You seem to want a template? template <std::size_t N> std::string hex_str(const std::array<uint8_t, N> array); Commented Oct 14, 2021 at 0:40
  • 1
    Are the sizes of the std::array known at compile time? If not, then no, the sized have to be known at compile time. What if instead you took iterators? That way someone can use whatever container they'd like. Commented Oct 14, 2021 at 0:45
  • 1
    Why not a single template that accepts both std::vector and std::array? Or even better, an iterator range? The whole point of standard containers having a consistent interface is for cases like this, so you can write a single algorithm that handles multiple container types equally Commented Oct 14, 2021 at 0:46
  • @Yksisarvinen yes. But I've struggled to write what I assume is fairly trivial code to actually do it. I've never written a template before Commented Oct 14, 2021 at 0:46

2 Answers 2

4

You should consider writing the function to work with iterators, like standard algorithms do. Then you can use it with both std::vector and std::array inputs, eg:

template<typename Iter>
std::string hex_str(Iter begin, Iter end)
{
    std::ostringstream output;
    output << std::hex << std::setw(2) << std::setfill('0');
    while(begin != end)
        output << static_cast<unsigned>(*begin++);
    return output.str();
}

Online Demo

If you really want to avoid having to call begin()/end() on whatever container you pass in, you can define a helper to handle that for you, eg:

template<typename C>
std::string hex_str(const C &data) {
    return hex_str(data.begin(), data.end());
}

Online Demo

Or, if you really wanted to, you can just flatten this all down into a single function, eg:

template <typename C>
std::string hex_str(const C& data)
{
    std::ostringstream output;
    output << std::hex << std::setw(2) << std::setfill('0');
    for(const auto &elem : data)
        output << static_cast<unsigned>(elem);
    return output.str();
}

Online Demo

Sign up to request clarification or add additional context in comments.

6 Comments

Can I just ask, and I don't mean to be rude, but do people actually write code using streams like that for real? It seems sarcastic to me, like you're saying "and this is why streams are a bad idea". I mean, at the very least you need to wrap it in "save stream state"..."restore stream state" (if you're doing that with cout).
@CodeAbominator what's wrong with the way this is written? There is no state worth "saving and restoring" in this case. What am I missing? Granted, the manipulators in this case don't really need to be repeated over and over, at least. I have updated my example regarding that.
compared to printf("%02x") it seems verbose and hard to understand. I suspect because I don't have a grasp of the extent of compiler magic involved it seems to me that "first make a stream. Then for each byte set the stream to have the characteristics you want, then output the byte to it. When you're done convert it to a string" is long-winded and likely to be slow as well as error-prone. It's not even so much that with a few hours work I can show that this exact detail works, it's that C++ seems to be about doing that a dozen times a day, every day.
What's the advantage of casting a u8 to a generic unsigned here? Is it that the native size is faster or is there some genuine "you should always" here? Like a compiler error if data is an invalid type for the cast or something?
@CodeAbominator "it seems verbose and hard to understand" - Whatever. The question is not about how to use streams, it is about how to implement hex_str(). I chose to use streams to utilize the std::hex manipulator. Use whatever implementation you want. "What's the advantage of casting a u8 to a generic unsigned here?" - operator<< outputs 8bit values as text characters, whether that be char, unsigned char, (u)int8_t , etc. We don't want that in this case, hence the cast to an int > 8bits. I could have used uint16_t, but unsigned is a more natural choice for the compiler
|
3

If you know the size of the std::array at compile time you can use a non type template parameter.

template<std::size_t N>
std::string hex_str( const std::array<std::uint8_t, N>& buffer )
{ /* Implementation */ }

int main( )
{   
    // Usage.
    std::array<std::uint8_t, 5> bytes = { 1, 2, 3, 4, 5 };
    const auto value{ hex_str( bytes ) };
}

Or you can just template the entire container (cut down on your overloads).

template<typename Container>
std::string hex_str( const Container& buffer ) 
{ /* Implementaion */ }

int main( )
{   
    // Usage.
    std::array<std::uint8_t, 5> bytes = { 1, 2, 3, 4, 5 };
    const auto value{ hex_str( bytes ) };
}

8 Comments

thank you so much. That looks suitably trivial and I'm glad I only wasted a couple of hours trying to do it before asking.
@CodeAbominator I edited the answer to show you how you can template the entire container as well.
If you have a std::array you can be sure you have the size at compile time. I prefer the iterator approach put forth by Remy above, but since that's off the table this is what I'd do.
And I imagine I can use the static_assert and type introspection stuff to limit the inputs to things that are actually valid.
@user4581301 I mentioned the iterator approach in the comments but the OP didn't like that idea.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.