# hexdump(): A memory layout utility to help debugging / understanding / optimising of C++ memory model

EDIT: Refer this answer for improved solution with incorporated feedback and extended functionality. eg Automatically hexdump both parts of a stack/heap obj like std::string.

## Inspiration / Motivation

Modern C++, certainly compared to C, provides a lot of abstractions which protects us from / hide from us the details of memory allocation and layout. Mostly "we don't care about the detail", or shouldn't. But sometimes we do for these reasons:

• New to C++: Should understand differences between stack & heap and have some simple appreciation about memory layout. Polymorphism?
• Debug and inspect your application, especially if you're doing something a bit unusual. Getting a clear picture of "what is happening in memory" can be helpful. Yes the debugger can provide that, but the commands can be obscure and it's hard to get an overview. Also, I can compile the code below without -g and with -O3 and it will not give me "optimized out" messages in the debugger. This is because the compiler "is aware of" the need to inspect the memory. (On the flip side we are changing what we are measuring here).
• Advanced optimisations like struct packing, or CPU cache line optimisations.

Before I go on: credit to this question and particularly this answer. They provided the idea and first cut of my code came from that answer.

## hd(adr, size): Show me the memory, quick!

Rather than battle a debugger, which is yet another abstraction, the idea is just to dump/inspect with:

std::cout << hd(&var, sizeof(var));


Sneak preview of the simple output (more interesting examples below):

0x405000: -- -- -- -- 31 32 33 34 35 36 37 38 39 30 00 --  | ....1234567890.

• We get the address (is it stack/heap/.text ?)
• The output is aligned to nearest 16 byte boundary => cache line? Padding between members or objects? (Most cache lines are 64 bytes, but that's too wide in terminal. Change one constant or make it a param is possibility).
• Hex and (printable) ASCII. Nothing new here.

The first 2 features are new over the related question / answer linked to above. In my opinion they broaden the use case significantly. Refer comments in main().

## The Code

Alert: Some of the code may hurt your eyes. I have tried to keep it C++'ish and tried (and failed) not to use too many reinterpret_casts. I have tried to avoid (too much) UB, and hopefully succeeded. But probably not. In the end, realistically, it's C really...?

Some features

• struct ostream_state should be self explanatory and could be more generally useful. Tried to keep this generic => templated.
• main function hex_dump() is a bit long, but has just 3 three (linked) stages. I decided not to split.
• Using the "evil" void* was a choice suggested by previous answer and I decided to stick with it for this application.
• Lots of pointer arithmetic and hence lots of // NOLINT to shut up clang-tidy. I have looked behind each of those, and I think they are all OK.
• Refer comments in main for demo of what I found interesting and useful in the output
• Caveat: Output will obviously differ per machine (incl the SBO limit).

Feedback wanted:

• I find this useful, but is it?
• Usual trawl for bugs / UB (oh oh!) / cleaner techniques or structure

#include <bits/stdint-intn.h>
#include <iomanip>
#include <iostream>
#include <memory>

// utility: keeps states of an ostream, restores on destruction
template <typename T>
struct ostream_state {
explicit ostream_state(std::basic_ostream<T>& stream)
: stream_{stream}, flags_{stream.flags()}, fill_{stream.fill()}, width_{stream.width()} {}

ostream_state(const ostream_state& other) = delete;
ostream_state& operator=(const ostream_state& other) = delete;

ostream_state(ostream_state&& other) = delete;
ostream_state& operator=(ostream_state&& other) = delete;

~ostream_state() {
stream_.flags(flags_);
stream_.fill(fill_);
stream_.width(width_);
}

private:
std::basic_ostream<T>&                    stream_;
std::ios_base::fmtflags                   flags_;
typename std::basic_ostream<T>::char_type fill_;
std::streamsize                           width_;
};

inline std::ostream& print_ptr(std::ostream& os, const void* ptr) {
auto state = ostream_state(os);
return os << std::setw(19) << std::setfill(' ') << ptr; // NOLINT
}

inline std::ostream& print_byte(std::ostream& os, const unsigned char* ptr) {
auto state = ostream_state(os);
return os << std::setw(2) << std::setfill('0') << std::hex << static_cast<unsigned>(*ptr) << ' ';
}

inline std::ostream& hex_dump(std::ostream& os, const void* buffer, std::size_t bufsize) {
if (buffer == nullptr || bufsize == 0) return os;

constexpr std::size_t maxline{16};

// buffer for printable characters
unsigned char  pbuf[maxline + 1]; // NOLINT
unsigned char* pbuf_curr{pbuf};   // NOLINT

const unsigned char* buf{reinterpret_cast<const unsigned char*>(buffer)}; // NOLINT

// pre-buffer area: floor(nearest maxline)
size_t      offset    = reinterpret_cast<size_t>(buffer) % maxline; // NOLINT
std::size_t linecount = maxline;
if (offset > 0) {
const void* prebuf = buf - offset; // NOLINT
print_ptr(os, prebuf) << ": ";
while (offset--) { // underflow OK NOLINT
os << "-- ";
*pbuf_curr++ = '.'; // NOLINT
--linecount;
}
}

// main buffer area
while (bufsize) {                                    // NOLINT
if (pbuf_curr == pbuf) print_ptr(os, buf) << ": "; // NOLINT
print_byte(os, buf);
*pbuf_curr++ = std::isprint(*buf) ? *buf : '.'; // NOLINT
if (--linecount == 0) {
*pbuf_curr++ = '\0';         // NOLINT
os << " | " << pbuf << '\n'; // NOLINT
pbuf_curr = pbuf;            // NOLINT
linecount = std::min(maxline, bufsize);
}
--bufsize;
++buf; // NOLINT
}

// post buffer area: finish incomplete line
if (pbuf_curr != pbuf) {                                                               // NOLINT
for (*pbuf_curr++ = '\0'; pbuf_curr != &pbuf[maxline + 1]; ++pbuf_curr) os << "-- "; // NOLINT
os << " | " << pbuf << '\n';                                                         // NOLINT
}
return os;
}

struct hd {
const void* buffer;
std::size_t bufsize;

hd(const void* buf, std::size_t bufsz) : buffer{buf}, bufsize{bufsz} {}

friend std::ostream& operator<<(std::ostream& out, const hd& hd) {
return hex_dump(out, hd.buffer, hd.bufsize);
}
};

// start of demo/test

struct Dummy {
int16_t a   = 0x1111; // forgive the ridiculous values, for easy spotting
int32_t b   = 0x22222222;
int32_t c   = 0x33333333;
void*   ptr = (void*)0x8888888888888888; // crazy!
};

int main() {
// Note on UB:
// It's UB to access + 1th byte of a sv or a str, but we know '\0'
// is there in all these cases. Tthe point of using the hd() utility is to
// examine these sorts of things during debugging so acceptably risky here?
auto sv1 = std::string_view{"1234567890"}; // in .text (ie code) segment => non-aligned
std::cout << hd(sv1.data(), sv1.size() + 1) << '\n';

auto sv2 = std::string_view{"Hello there this is a much longer string"};
std::cout << hd(sv2.data(), sv2.size() + 1) << '\n';  // starts on next byte after sv1

auto str1 = std::string{"123456789012345"}; // SBO on stack => non-aligned
std::cout << hd(str1.data(), str1.size() + 1) << '\n';

auto str2 = std::string{"1234567890123456"}; // too big for SBO => on heap => 16Byte aligned
std::cout << hd(str2.data(), str2.size() + 1) << '\n';

auto d1 = Dummy{}; // on stack with alignment gaps
std::cout << hd(&d1, sizeof(d1)) << '\n';

auto d2 = std::make_unique<Dummy>(); // on heap with alignment gaps
std::cout << hd(d2.get(), sizeof(*d2)) << '\n';

auto d3 = std::make_unique<Dummy[]>(4); // array on heap: odd/even alignment NOLINT
std::cout << hd(d3.get(), 4 * sizeof(d3[0])) << '\n';


Output:

           0x405000: -- -- -- -- 31 32 33 34 35 36 37 38 39 30 00 --  | ....1234567890.

0x405000: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 48  | ...............H
0x405010: 65 6c 6c 6f 20 74 68 65 72 65 20 74 68 69 73 20  | ello there this
0x405020: 69 73 20 61 20 6d 75 63 68 20 6c 6f 6e 67 65 72  | is a much longer
0x405030: 20 73 74 72 69 6e 67 00 -- -- -- -- -- -- -- --  |  string.

0x7ffe99b799f0: -- -- -- -- -- -- -- -- 31 32 33 34 35 36 37 38  | ........12345678
0x7ffe99b79a00: 39 30 31 32 33 34 35 00 -- -- -- -- -- -- -- --  | 9012345.

0x131bec0: 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36  | 1234567890123456
0x131bed0: 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --  | .

0x7ffe99b79950: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x7ffe99b79960: 88 88 88 88 88 88 88 88 -- -- -- -- -- -- -- --  | ........

0x131bee0: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x131bef0: 88 88 88 88 88 88 88 88 -- -- -- -- -- -- -- --  | ........

0x131bf00: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x131bf10: 88 88 88 88 88 88 88 88 11 11 00 00 22 22 22 22  | ............""""
0x131bf20: 33 33 33 33 00 00 00 00 88 88 88 88 88 88 88 88  | 3333............
0x131bf30: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x131bf40: 88 88 88 88 88 88 88 88 11 11 00 00 22 22 22 22  | ............""""
0x131bf50: 33 33 33 33 00 00 00 00 88 88 88 88 88 88 88 88  | 3333............

• Very related: codereview.stackexchange.com/questions/165120/… – πάντα ῥεῖ Jan 22 '20 at 19:12
• Yes, exactly, I linked to it at top and hopefully gave appropriate credit. This is a further development of the idea. – Oliver Schönrock Jan 22 '20 at 19:14
• Just noticed after seeing what you linked. – πάντα ῥεῖ Jan 22 '20 at 19:14
• Please stop editing your question. You have an answer now, and your edits may invalidate it. If any invalidation happens we will side with the answerer as posting a question prematurely is considered poor form. Thank you. – Peilonrayz Jan 22 '20 at 21:09
• @OliverSchonrock I didn't delete any of your comments. I don't understand why you seem to accuse me of random things. But honestly I don't care. Bye. – Peilonrayz Jan 22 '20 at 21:16

# Usefulness

Looks useful, although you could achieve something similar with a debugger, which in the end is much more powerful. See this post about making GDB give similar output.

# Make a C++20 version

Because the code would be much cleaner if you could use std::format(). You can make it backwards compatible by detecting if C++20 support is present, otherwise falling back to fmtlib.

# Consider making hd() take a reference

I think it would be much cleaner if hd() took a reference, and have the size default to the size of the type:

template<typename T>
hd(const T &buf, std::size_t bufsz = sizeof(T)): buffer{...}, bufsize{...} {}


And then you could use it like:

auto d1 = Dummy{};
std::cout << hd(d1) << '\n';


Of course, it gets a bit harder to use if you want to dump arbitrary memory, but it's a trade-off. I think it would benefit beginners, who may not know the size of types, or might easily make mistakes like writing hd(some_pointer, sizeof(some_pointer)). Maybe have two distinct classes, or have a version that takes only a reference and no size, and one that takes a void pointer and a size.

# Avoid void * where possible

I think it's best if you cast to unsigned char * as soon as possible (in hd's constructor), and stick with it. This avoids some casts in other parts of your code. The only time you want to cast it again is when printing the value of a pointer in print_ptr().

Even better, use std::byte *, and only convert a std::byte to char when needing to print it as a character directly.

# Organizing the code

The function hex_dump() is a bit messy. It would be nicer if it was split into more functions, delegating more work to print_*() functions. In particular, have print_bytes() take a length so it prints a range of bytes in one go. Add a print_ascii() that prints a range of bytes as printable ASCII characters.

You could move the handling of the pre- and post-buffer area to the print_*() functions, avoiding the special-casing in hex_dump() itself. For example:

constexpr std::size_t linesize{16};
auto buf{buffer};
size_t pre = reinterpret_cast<size_t>(buffer) % maxline; // Size of pre-buffer area
buf -= pre; // Align buf and bufsize to start of pre-buffer area
bufsize += pre;

while (bufsize) {
// Calculate the length of the post-buffer area on this line
size_t post = bufsize < linesize ? linesize - bufsize : 0;

// Print the line
print_ptr(os, buf);
os << ": ";
print_bytes(os, buf, linesize, pre, post);
os << " | ";
print_ascii(os, buf, linesize, pre, post);
os << "\n";

buf += linesize;
bufsize -= linesize - post; // Advance to next line, avoiding wrapping
pre = 0;                    // No pre-buffer area after first line
}


I can think of use cases where you'd want to see the pointers as decimal values, possibly even just offsets from the start of the data instead of absolute values. Perhaps this could be made configurable?

# Make bufsize_const

Just as buffer_ is const, you can make bufsize_ const as well.

# Implications of using a class instead of a function

Using a class that has an operator<<() overload is a way to get relatively optimized output to an std::ostream, but there are some consequences. First, the class holds references to the data that is to be printed, and there is no guarantee that the data will be valid at the time you actually print it. For example, I could write:

hd bad_hd() {
return hd(&i, sizeof i);
}
...


This is a similar to how std::span behaves. While in the above it is rather obvious you are taking a reference of a local variable, it is harder to see if you have an overload that takes a reference of an arbitrary type. If you would use a function instead (basically hex_dump(), possibly with overloads), you would not have this issue.

The second issue, which is very minor but might surprise someone, is that you can't use hd() to print an instance of itself using the reference template overload:

int foo = 0xf00;
auto foo_hd = hd(foo);
std::cout << foo_hd; // OK
std::cout << hd(foo_hd); // Prints a copy of the above
std::cout << hd(&foo_hd, sizeof foo_hd); // Works

• Great stuff as usual. 2 things. 1. I struggled to get anything much to work with std::byte (due to amount of weird recasting here). I will try again. and 2. That's a good link to gdb post. I suspected some "scripting" could do that. TBH I find gdb (and any debugger even the IDE ones) to be a pain. It takes me so long to get set up the way I want it, and I need to so rarely that in 99.9% of cases it's faster to "print and think". What do you find? – Oliver Schönrock Jan 22 '20 at 21:18
• It depends. Sure, it's easy to just add a print-statement here and there (and I do this too often myself), but then you need to recompile and rerun your program, and maybe you forgot to add a print-statement or made another mistake, and then you have to repeat the process, and in the end it would've been faster if you'd used the debugger from the beginning. It's definitely worth it to learn how to use one. Don't try too hard to get it to set it up the way you want it, learn how it works out of the box first. – G. Sliepen Jan 22 '20 at 21:42
• Yeah. I do that. I am not too bad at gdb. I just find too many commands are required. Breakpoints, steps into out of, watches "examines" etc ...Oh now I need to recompile with -O0 because the variable has been optimised out. By that stage I have solved it by thinking carefully about targeted prints and recompiling once. Mind you my compile times tend to be measured in seconds, not minutes. It's horses for course really. I find 99% I am in the print world. – Oliver Schönrock Jan 22 '20 at 21:52
• About the "correct" size: it depends on what you want. If I have some POD class I want to dump exactly that, and then sizeof(T) is what you want. But if you want it to print the dynamically allocated parts of a container, then you'd have to specify the size and right pointer manually. Or... you could add some SFINAE overloads to make it detect if T::data() and T::size() exist and then use those? – G. Sliepen Jan 22 '20 at 22:14
• refactor went well. thanks. On "reference": I am going down a double route. Keep the 2 param (ptr and size) approach as "power tool" and have a templated reference single param overload for which we have complete template specialisations for common non-trivial types, eg: template <typename T> explicit hd(const T& buf) : buffer{&buf}, bufsize{sizeof(T)} {} template <> explicit hd(const std::string_view& buf) : buffer{buf.data()}, bufsize{buf.size() + 1} {} The unspecialiased version handles int etc. I am considering doing 2 dumps for std::string: stack part / heap part. – Oliver Schönrock Jan 23 '20 at 0:05

A refactored improved version taking on board many of the points from @G.Sliepen's answer and then building on them:

• No more void* except for the general form ctor of hd
• Main function hex_dump() totally refactored. Better.
• hd ctor now has 2 overloads.

There is a general form of hd ctor with (adr, size) and then a templated form which takes a reference. The general for of that template calls sizeof() and uses &var for the address.

But there are also specialisations for std::string_view and std::string which:

1. do the same as the general form (gives the stack part of the obj)
2. Then chain a new instance of hd for the heap part of the object

I am still validating this design. It seems to work fine. Feedback welcome. When bedded down, I should be able to extend it to say std::vector: Printing the stack and heaps parts of the container. It might even be able to recurse down multiple layers.

New code and output below.

Small surprise: libstd++ uses 32bytes of stack for a heap allocated string? A .data() ptr a size() and another size which turns out to be the .capacity(). The remaining 8 bytes are alignment junk? Anyway we adjusted code to show the whole capacity of buffer which has been reserved(). Not just the current size().

#include <bits/stdint-intn.h>
#include <iomanip>
#include <iostream>
#include <memory>

// utility: keeps states of an ostream, restores on destruction
template <typename T>
struct ostream_state {
explicit ostream_state(std::basic_ostream<T>& stream)
: stream_{stream}, flags_{stream.flags()}, fill_{stream.fill()} {}

ostream_state(const ostream_state& other) = delete;
ostream_state& operator=(const ostream_state& other) = delete;

ostream_state(ostream_state&& other) = delete;
ostream_state& operator=(ostream_state&& other) = delete;

~ostream_state() {
stream_.flags(flags_);
stream_.fill(fill_);
}

private:
std::basic_ostream<T>&                    stream_;
std::ios_base::fmtflags                   flags_;
typename std::basic_ostream<T>::char_type fill_;
};

namespace detail {
os << std::setw(19) << std::setfill(' ') << adr; // NOLINT
}

inline void print_fill_advance(std::ostream& os, const std::byte*& buf, std::size_t cnt,
const std::string& str) {
while (cnt-- != 0U) {
++buf; // NOTE: unusually this in passsed in by ref and we advance it. NOLINT
os << str;
}
}

inline void print_hex(std::ostream& os, const std::byte* buf, std::size_t linesize, std::size_t pre,
std::size_t post) {
{
os << std::setfill('0') << std::hex;
auto cnt = linesize - pre - post;
while (cnt-- != 0U) os << std::setw(2) << static_cast<unsigned>(*buf++) << ' '; // NOLINT
}
}

inline void print_ascii(std::ostream& os, const std::byte* buf, std::size_t linesize,
std::size_t pre, std::size_t post) {
auto cnt = linesize - pre - post;
while (cnt-- != 0U) {
os << (std::isprint(static_cast<unsigned char>(*buf)) != 0 ? static_cast<char>(*buf) : '.');
++buf; // NOLINT
}
}
} // namespace detail

inline std::ostream& hex_dump(std::ostream& os, const std::byte* buffer, std::size_t bufsize) {
if (buffer == nullptr || bufsize == 0) return os;

constexpr std::size_t linesize{16};
const std::byte*      buf{buffer};
std::size_t           pre =
reinterpret_cast<std::size_t>(buffer) % linesize; // Size of pre-buffer area  NOLINT
bufsize += pre;
buf -= pre; // NOLINT

auto state = ostream_state{os}; // save stream setting and restore at end of scope
while (bufsize != 0U) {
std::size_t post = bufsize < linesize ? linesize - bufsize : 0;

os << ": ";
detail::print_hex(os, buf, linesize, pre, post);
os << " | ";
detail::print_ascii(os, buf, linesize, pre, post);
os << "\n";

buf += linesize; // NOLINT
bufsize -= linesize - post;
pre = 0;
}
return os;
}

class hd {
public:
hd(const void* buf, std::size_t bufsz)
: buffer_{reinterpret_cast<const std::byte*>(buf)}, bufsize_{bufsz} {}

template <typename T>
explicit hd(const T& buf)
: buffer_{reinterpret_cast<const std::byte*>(&buf)}, bufsize_{sizeof(T)} {}

// It's UB to access + 1th byte of a string_view so we don't, despite most
// targets of string_views (ie std::string or string literal) having '\0'.
template <>
explicit hd(const std::string_view& buf)
: buffer_{reinterpret_cast<const std::byte*>(&buf)}, bufsize_{sizeof(buf)} {
child_       = std::make_unique<hd>(buf.data(), buf.size());
child_label_ = "string viewed";
}

// There is some debate but we believe str[size()] is legal via [] or *
// but UB via iterator. So here we DO show the '0' terminator.
template <>
explicit hd(const std::string& buf)
: buffer_{reinterpret_cast<const std::byte*>(&buf)}, bufsize_{sizeof(buf)} {
auto data_byte_ptr = reinterpret_cast<const std::byte*>(buf.data());
if (!(data_byte_ptr > buffer_ && data_byte_ptr < buffer_ + bufsize_)) {
// not SBO, show the real string as well
child_ = std::make_unique<hd>(buf.data(), buf.size() + 1);
child_label_ = "heap string";
}
}

friend std::ostream& operator<<(std::ostream& os, const hd& hd) {
hex_dump(os, hd.buffer_, hd.bufsize_); // NOLINT
if (hd.child_) os << std::setw(19) << hd.child_label_ << ":\n" << *(hd.child_);
return os;
}

private:
const std::byte* buffer_;
std::size_t      bufsize_;

std::unique_ptr<hd> child_ = nullptr;
std::string         child_label_;
};

// start of demo/test

struct Dummy {
int16_t a      = 0x1111;
int32_t b      = 0x22222222;
int32_t c      = 0x33333333;
void*   end    = (void*)0xffffffffffffffff; // NOLINT end of earth
};

int main() {
auto i1 = int{0x12345678}; // 4 bytes int, 4-byte aligned
std::cout << os::hd(i1) << '\n';

auto pi = 22/7.0; // 8bytes on stack
std::cout << os::hd(pi) << '\n';

auto sv1 = std::string_view{"1234567890"}; // in .text (ie code) segment => non-aligned
std::cout << os::hd(sv1) << '\n';

auto sv2 = std::string_view{"This is a much longer string view onto a string literal"};
std::cout << os::hd(sv2) << '\n'; // starts after sv1 with '\0' gap

auto i2 = short{0x1234};  // 2 bytes int, 4-byte aligned
std::cout << os::hd(i2) << '\n';

auto str1 = std::string{"123456789012345"}; // SBO on stack => 16-byte aligned??
std::cout << os::hd(str1) << '\n';

auto str2 = std::string{"1234567890123456"}; // too big for SBO => on heap => 16Byte aligned
std::cout << os::hd(str2) << '\n';

auto d1 = Dummy{}; // on stack 8-byte aligned with padding gaps
std::cout << os::hd(d1) << '\n';

auto d2 = std::make_unique<Dummy>(); // on heap 16byte aligned with padding gaps
std::cout << os::hd(d2.get(), sizeof(*d2)) << '\n';

auto d3 = std::make_unique<Dummy[]>(4); // array on heap 8-byte aligned: odd/even NOLINT
std::cout << os::hd(d3.get(), 4 * sizeof(d3[0])) << '\n';
}


Output

     0x7ffcb8d3d350: -- -- -- -- 78 56 34 12 -- -- -- -- -- -- -- --  | ....xV4.........

0x7ffcb8d3d370: -- -- -- -- -- -- -- -- 49 92 24 49 92 24 09 40  | ........I.$I.$.@

0x7ffcb8d3d3a0: -- -- -- -- -- -- -- -- 0a 00 00 00 00 00 00 00  | ................
0x7ffcb8d3d3b0: 42 66 42 00 00 00 00 00 -- -- -- -- -- -- -- --  | BfB.............
string viewed:
0x426640: -- -- 31 32 33 34 35 36 37 38 39 30 -- -- -- --  | ..1234567890....

0x7ffcb8d3d390: -- -- -- -- -- -- -- -- 37 00 00 00 00 00 00 00  | ........7.......
0x7ffcb8d3d3a0: 4d 66 42 00 00 00 00 00 -- -- -- -- -- -- -- --  | MfB.............
string viewed:
0x426640: -- -- -- -- -- -- -- -- -- -- -- -- -- 54 68 69  | .............Thi
0x426650: 73 20 69 73 20 61 20 6d 75 63 68 20 6c 6f 6e 67  | s is a much long
0x426660: 65 72 20 73 74 72 69 6e 67 20 76 69 65 77 20 6f  | er string view o
0x426670: 6e 74 6f 20 61 20 73 74 72 69 6e 67 20 6c 69 74  | nto a string lit
0x426680: 65 72 61 6c -- -- -- -- -- -- -- -- -- -- -- --  | eral............

0x7ffcb8d3d320: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 34 12  | ..............4.

0x7ffcb8d3d330: 40 d3 d3 b8 fc 7f 00 00 0f 00 00 00 00 00 00 00  | @...............
0x7ffcb8d3d340: 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 00  | 123456789012345.

0x7ffcb8d3d350: -- -- -- -- -- -- -- -- 90 f8 e1 01 00 00 00 00  | ................
0x7ffcb8d3d360: 10 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00  | ................
0x7ffcb8d3d370: 08 e0 04 eb 00 7f 00 00 -- -- -- -- -- -- -- --  | ................
heap string:
0x1e1f890: 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36  | 1234567890123456
0x1e1f8a0: 00 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --  | ................

0x7ffcb8d3d380: 11 11 42 00 22 22 22 22 33 33 33 33 00 00 00 00  | ..B.""""3333....
0x7ffcb8d3d390: ff ff ff ff ff ff ff ff -- -- -- -- -- -- -- --  | ................

0x1e1f910: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x1e1f920: ff ff ff ff ff ff ff ff -- -- -- -- -- -- -- --  | ................

0x1e11eb0: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x1e11ec0: ff ff ff ff ff ff ff ff 11 11 00 00 22 22 22 22  | ............""""
0x1e11ed0: 33 33 33 33 00 00 00 00 ff ff ff ff ff ff ff ff  | 3333............
0x1e11ee0: 11 11 00 00 22 22 22 22 33 33 33 33 00 00 00 00  | ....""""3333....
0x1e11ef0: ff ff ff ff ff ff ff ff 11 11 00 00 22 22 22 22  | ............""""
0x1e11f00: 33 33 33 33 00 00 00 00 ff ff ff ff ff ff ff ff  | 3333............

• Nice refactoring! I noticed you can static_cast<>() a void * to a std::byte * instead of using a reinterpret_cast<>(). Also, in the loop in print_ascii(), you could write auto c = std::to_integer<char>(*buf++); os << (std::isprint(c) ? c : '.') (you need to #include <cstddef>). Using std::unique_ptr<hd> child_ is interesting, but why not just have two buffer and bufsize variables, and call hex_dump() twice in operator<<()? This avoids memory allocations. – G. Sliepen Jan 23 '20 at 22:04
• @G.Sliepen Thanks. Good call on static_cast (only for the general ctor though). And good call on std::to_integer. Wasn't aware of that. But is it correct? Not UB? I thought isprint needs unsigned char to be non-UB? – Oliver Schönrock Jan 23 '20 at 22:42
• @G.Sliepen On chaining: I agree the allocation is slightly irritating. I might go three levels though or even recursive so that the idea of a straight "clone" of the object "and recurse" (sort of, not quite yet) appealed to me. The only other solution I thought of was to give hd() a default ctor. Then "empty - value initalialised" could mean "no child" (with nullptr / zero), and then you popluate it if you need it...? That way we always get an extra (often uneeded) hd obj, but that's cheap? --- I have done a std::vector version too. These are quite trivial to add. – Oliver Schönrock Jan 23 '20 at 22:42
• @G.Sliepen Since you are the casting expert, do you have ideas on my char* struggles? codereview.stackexchange.com/questions/236073/… See comments under first answer for more details. – Oliver Schönrock Jan 23 '20 at 23:16
• You're right apparently about std::isprint() needing an unsigned char, although in practice I've never seen any code casting a char to unsigned char before using any of the ctype.h functions. But better safe than sorry. You can't have a hd object directly embedded in hd itself, it always has to be a pointer, so then using std::unique_ptr` is the right thing to do. – G. Sliepen Jan 24 '20 at 19:49