fourBitsToString
and charToHex
Both of these functions are basically reimplementations of existing features of the <iostream>
and <iomanip>
headers.
The whole call to charToHex
could simply be replaced by std::cout << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(c);
.
Note the static_cast<int>
: We want to print the numerical value, not the ASCII character glyph!
However, setting std::uppercase
, std::hex
and std::setfill('0')
for every byte to be printed would be wasteful, plus it would overwrite any previously set flags on std::cout
.
A better approach would be using a intermediary std::ostringstream
instead, setting the flags once and retrieving (and resetting) its contents when sufficiently filled.
More on that in the section below.
Also, both functions end with return move(s);
. This seems like misguided premature optimization ("Hey, we can move here! No need for a copy!"), but it likely is actually a pessimization instead!
Compilers actually know that the returned value is a temporary and can optimize for this case (usually called NRVO - "Named return value optimization"). But: They can only do this if the variable is returned directly, and the call to std::move
prevents that.
Prefer return s;
, unless you have good reasons (e.g. measurements/bad compiler) indicating otherwise.
PrintMBR
This function is a big pile of unnecessarily complicated code with poorly documented/enfored preconditions.
First off, let's take a look at the function signature:
static void PrintMBR(char buffer[MBR_SIZE])
The flaw is really subtle: buffer
isn't actually restricted to arrays of size MBR_SIZE
at all! It's basically just a slightly fancier markup of static void PrintMBR(char *buffer)
, and the compiler will actually treat it as such.
This allows PrintMBR
to be called with char
arrays of all possible sizes, including smaller than MBR_SIZE
, or even just nullptr
.
How to fix this?
Well, the first inclination might be to just limit the size of buffer
by explicitly making it keep track of the size information, e.g. by using a reference to a C-style array char (&buffer)[MBR_SIZE]
or a fancier std::array
reference const std::array<char, MBR_SIZE> &buffer
.
And that is a valid option if the only purpose was to print MBR contents.
But looking at it's actually implementation, it seems like the intention was to print the hex values in one big column next to a column containing to the ASCII characters (if printable) nicely aligned. This could be generalized for printing arbitrary char
arrays by amending some of the assumptions made further down in the implementation.
Next, let's have a look at the function body:
size_t byteIndex = 0;
string lineSeparator;
string columnSeparator;
for (size_t i = 0; i < NUMBER_OF_LINES; i++) {
cout << lineSeparator;
lineSeparator = '\n';
columnSeparator = "";
for (size_t j = 0; j < BYTES_PER_LINE; j++) {
string ch = charToHex(buffer[byteIndex++]);
cout << columnSeparator << ch;
columnSeparator = " ";
}
cout << ' ';
for (size_t j = 0; j < BYTES_PER_LINE; j++) {
char c = buffer[byteIndex - BYTES_PER_LINE + j];
cout << (isprint((unsigned int) c) ? c : '.');
}
}
If I understand correctly, the intention is to print the MBR in a XX XX XX XX xxxx
line format (where X
is a hex digit and x
is an ASCII character).
First off, it's hard to keep track of "constants" if their values are constantly changing. With one more check and a tiny bit of reordering, we can fix this:
size_t byteIndex = 0;
static const string lineSeparator = "\n";
static const string columnSeparator = " ";
for (size_t i = 0; i < NUMBER_OF_LINES; i++) {
if(i != 0) cout << lineSeparator;
for (size_t j = 0; j < BYTES_PER_LINE; j++) {
string ch = charToHex(buffer[byteIndex++]);
cout << ch << columnSeparator;
}
for (size_t j = 0; j < BYTES_PER_LINE; j++) {
char c = buffer[byteIndex - BYTES_PER_LINE + j];
cout << (isprint((unsigned int) c) ? c : '.');
}
}
A bit more readable, and I don't have to keep track of all the possible states (and weird reassignments).
I mentioned above that instead of using charToHex
, one could use std::ostringstream
and <iomanip>
facilities instead:
void PrintMBR(char (&buffer)[MBR_SIZE]) {
static constexpr auto bytes_per_line = 4;
static const auto column_delimiter = " "s;
static const auto line_delimiter = "\n"s;
auto hex_part = std::ostringstream{};
auto ascii_part = std::ostringstream{};
auto counter = 0;
hex_part << std::uppercase << std::hex << std::setfill('0');
for(auto c : buffer)
{
hex_part << std::setw(2) << static_cast<int>(c) << column_delimiter;
ascii_part << (isprint(static_cast<unsigned int>(c)) ? c : '.');
++counter;
if(counter % bytes_per_line == 0)
{
std::cout << hex_part.str() << ascii_part.str() << line_delimiter;
hex_part.str("");
ascii_part.str("");
}
}
}
This could easily be made more generic to allow printing any byte buffer in this format.
ReadMBR
Memory leak: buffer
doesn't ever get used after being allocated, including being deleted.
I don't like the return GetLastError();
bit. I guess it's fine in pure C, but in C++ there is a more common way to communicate error conditions: exceptions.
I get this is kind of a glue layer between C WinAPI and C++, but that doesn't mean you have to port idioms from one side into the other, especially if there are more idiomatic alternatives.
I see this as problematic in this case, as likely nobody checks return codes unless they have to. Case in point: main()
.
To be more generic, I would really like for this method to accept an OutputIterator
. But sadly, the C WinAPI doesn't know those, so that would require another copy of the data (using an intermediary buffer). That's one of the design trade-offs: Performance vs. Usability.
main
- No check on the return code of
ReadMBR
.
return 3;
- What does this 3
represent?