Skip to content

Commit

Permalink
Utility: add a Debug::hex modifier.
Browse files Browse the repository at this point in the history
Finally, after about a decade of procrastination. It's amazing that even
in such a trivial / well-supported use case of STL iostreams I ran into
a weird behavior where std::hex prints negative numbers as unsigned. Who
asked for such a behavior?!
  • Loading branch information
mosra committed Mar 5, 2024
1 parent c179743 commit 718e5fd
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 47 deletions.
2 changes: 2 additions & 0 deletions doc/corrade-changelog.dox
Expand Up @@ -208,6 +208,8 @@ namespace Corrade {
and @relativeref{Utility::ConfigurationGroup,groups()}
- New @ref Utility::Debug::invertedColor() output modifier for printing
colored text with the foreground and background colors inverted
- New @ref Utility::Debug::hex output modifier for printing integers as
hexadecimal
- New @ref Corrade::Utility::Json class for tokenizing and parsing JSON files
into an immutable memory-efficient representation. See also
[mosra/corrade#174](https://github.com/mosra/corrade/issues/174).
Expand Down
7 changes: 7 additions & 0 deletions doc/snippets/Utility.cpp
Expand Up @@ -549,6 +549,13 @@ Utility::Debug{Utility::Debug::Flag::NoNewlineAtTheEnd} << "Hello!";
/* [Debug-modifiers-whitespace] */
}

{
/* [Debug-modifiers-base] */
// Prints 0xc0ffee
Utility::Debug{} << Utility::Debug::hex << 0xc0ffee;
/* [Debug-modifiers-base] */
}

{
/* [Debug-modifiers-colors] */
Utility::Debug{}
Expand Down
43 changes: 28 additions & 15 deletions src/Corrade/Utility/Debug.cpp
Expand Up @@ -365,26 +365,26 @@ void Debug::resetColor(Debug& debug) {
debug.resetColorInternal();
}

namespace { enum: unsigned char { PublicFlagMask = 0x1f }; }
namespace { enum: unsigned short { PublicFlagMask = 0x00ff }; }

auto Debug::flags() const -> Flags {
return Flag(static_cast<unsigned char>(_flags) & PublicFlagMask);
return Flag(static_cast<unsigned short>(_flags) & PublicFlagMask);
}

void Debug::setFlags(Flags flags) {
_flags = InternalFlag(static_cast<unsigned char>(flags)) |
InternalFlag(static_cast<unsigned char>(_flags) & ~PublicFlagMask);
_flags = InternalFlag(static_cast<unsigned short>(flags)) |
InternalFlag(static_cast<unsigned short>(_flags) & ~PublicFlagMask);
}

auto Debug::immediateFlags() const -> Flags {
return Flag(static_cast<unsigned char>(_immediateFlags) & PublicFlagMask) |
Flag(static_cast<unsigned char>(_flags) & PublicFlagMask);
return Flag(static_cast<unsigned short>(_immediateFlags) & PublicFlagMask) |
Flag(static_cast<unsigned short>(_flags) & PublicFlagMask);
}

void Debug::setImmediateFlags(Flags flags) {
/* unlike _flags, _immediateFlags doesn't contain any internal flags so
no need to preserve these */
_immediateFlags = InternalFlag(static_cast<unsigned char>(flags));
_immediateFlags = InternalFlag(static_cast<unsigned short>(flags));
}

std::ostream* Debug::defaultOutput() { return &std::cout; }
Expand Down Expand Up @@ -452,7 +452,7 @@ bool Debug::isTty() { return isTty(debugGlobals.output); }
bool Warning::isTty() { return Debug::isTty(debugGlobals.warningOutput); }
bool Error::isTty() { return Debug::isTty(debugGlobals.errorOutput); }

Debug::Debug(std::ostream* const output, const Flags flags): _flags{InternalFlag(static_cast<unsigned char>(flags))}, _immediateFlags{InternalFlag::NoSpace} {
Debug::Debug(std::ostream* const output, const Flags flags): _flags{InternalFlag(static_cast<unsigned short>(flags))}, _immediateFlags{InternalFlag::NoSpace} {
/* Save previous global output and replace it with current one */
_previousGlobalOutput = debugGlobals.output;
debugGlobals.output = _output = output;
Expand Down Expand Up @@ -565,21 +565,30 @@ template<class T> Debug& Debug::print(const T& value) {
}
#endif

/* Separate values with spaces if enabled; reset all internal flags after */
/* Separate values with spaces if enabled */
if(!((_immediateFlags|_flags) & InternalFlag::NoSpace))
*_output << ' ';
_immediateFlags = {};
/* Print the next value as hexadecimal if enabled */
/** @todo this does strange crap for negative values (printing them as
unsigned), revisit once iostreams are not used anymore */
if(((_immediateFlags|_flags) & InternalFlag::Hex) && std::is_integral<T>::value)
*_output << "0x" << std::hex;

toStream(*_output, value);

/* Reset the hexadecimal printing back if it was enabled */
if(((_immediateFlags|_flags) & InternalFlag::Hex) && std::is_integral<T>::value)
*_output << std::dec;

/* Reset all internal flags after */
_immediateFlags = {};

_flags |= InternalFlag::ValueWritten;
return *this;
}

Debug& Debug::operator<<(const void* const value) {
std::ostringstream o;
o << "0x" << std::hex << reinterpret_cast<std::uintptr_t>(value);
return print(o.str());
return *this << hex << reinterpret_cast<std::uintptr_t>(value);
}

Debug& Debug::operator<<(const char* value) { return print(value); }
Expand Down Expand Up @@ -698,11 +707,13 @@ Debug& operator<<(Debug& debug, Debug::Flag value) {
_c(NoSpace)
_c(Packed)
_c(Color)
/* Space reserved for Bin and Oct */
_c(Hex)
#undef _c
/* LCOV_EXCL_STOP */
}

return debug << "Utility::Debug::Flag(" << Debug::nospace << reinterpret_cast<void*>(static_cast<unsigned char>(char(value))) << Debug::nospace << ")";
return debug << "Utility::Debug::Flag(" << Debug::nospace << reinterpret_cast<void*>(static_cast<unsigned short>(value)) << Debug::nospace << ")";
}

Debug& operator<<(Debug& debug, Debug::Flags value) {
Expand All @@ -711,7 +722,9 @@ Debug& operator<<(Debug& debug, Debug::Flags value) {
Debug::Flag::DisableColors,
Debug::Flag::NoSpace,
Debug::Flag::Packed,
Debug::Flag::Color});
Debug::Flag::Color,
/* Space reserved for Bin and Oct */
Debug::Flag::Hex});
}
#endif

Expand Down
58 changes: 48 additions & 10 deletions src/Corrade/Utility/Debug.h
Expand Up @@ -115,6 +115,14 @@ newline at the end --- use @ref Flag::NoNewlineAtTheEnd or the @ref nospace
@snippet Utility.cpp Debug-modifiers-whitespace
@subsection Utility-Debug-modifiers-base Printing numbers in a different base
With @ref Flag::Hex or the @ref hex modifier, integers will be printed as
hexadecimal. Pointer values are printed as hexadecimal always, cast them to an
integer type to print them as decimal.
@snippet Utility.cpp Debug-modifiers-base
@subsection Utility-Debug-modifiers-colors Colored output
It is possible to color the output using @ref color(), @ref boldColor() and
Expand Down Expand Up @@ -207,7 +215,7 @@ class CORRADE_UTILITY_EXPORT Debug {
*
* @see @ref Flags, @ref Debug(Flags)
*/
enum class Flag: unsigned char {
enum class Flag: unsigned short {
/** Don't put newline at the end on destruction */
NoNewlineAtTheEnd = 1 << 0,

Expand Down Expand Up @@ -237,7 +245,18 @@ class CORRADE_UTILITY_EXPORT Debug {
* Print colored values as colored squares in the terminal.
* @see @ref color, @ref operator<<(unsigned char)
*/
Color = 1 << 4
Color = 1 << 4,

/* Bit 5 and 6 reserved for Bin and Oct */

/**
* Print integer values as lowercase hexadecimal prefixed with
* `0x`, e.g. @cb{.shell-session} 0xc0ffee @ce instead of
* @cb{.shell-session} 12648430 @ce.
* @see @ref hex, @ref operator<<(const void*)
* @m_since_latest
*/
Hex = 1 << 7

/* When adding values, don't forget to adapt InternalFlag as well
and update PublicFlagMask in Debug.cpp */
Expand Down Expand Up @@ -463,6 +482,19 @@ class CORRADE_UTILITY_EXPORT Debug {
debug._immediateFlags |= InternalFlag::Color;
}

/**
* @brief Print the next value as hexadecimal
* @m_since_latest
*
* If the next value is integer, it's printed as lowercase hexadecimal
* prefixed with `0x` e.g. @cb{.shell-session} 0xc0ffee @ce instead of
* @cb{.shell-session} 12648430 @ce.
* @see @ref Flag::Hex, @ref operator<<(const void*)
*/
static void hex(Debug& debug) {
debug._immediateFlags |= InternalFlag::Hex;
}

/**
* @brief Debug output modification
*
Expand Down Expand Up @@ -646,10 +678,13 @@ class CORRADE_UTILITY_EXPORT Debug {
/**
* @brief Print a pointer value to debug output
*
* The value is printed in lowercase hexadecimal, for example
* @cb{.shell-session} 0xdeadbeef @ce.
* The value is printed in lowercase hexadecimal prefixed with `0x`,
* for example @cb{.shell-session} 0xdeadbeef @ce. Equivalent to
* enabling @ref Flag::Hex or using the @ref hex modifier and printing
* @cpp reinterpret_cast<std::uintptr_t>(value) @ce instead of
* @cpp value @ce.
*/
Debug& operator<<(const void* value); /**< @overload */
Debug& operator<<(const void* value);

/**
* @brief Print a boolean value to debug output
Expand Down Expand Up @@ -756,15 +791,18 @@ class CORRADE_UTILITY_EXPORT Debug {
#endif
std::ostream* _output;

enum class InternalFlag: unsigned char {
/* Values compatible with Flag enum */
enum class InternalFlag: unsigned short {
/* Values matching the Flag enum */
NoNewlineAtTheEnd = 1 << 0,
DisableColors = 1 << 1,
NoSpace = 1 << 2,
Packed = 1 << 3,
Color = 1 << 4,
ValueWritten = 1 << 5,
ColorWritten = 1 << 6
/* Bit 5 and 6 reserved for Bin and Oct */
Hex = 1 << 7,

ValueWritten = 1 << 8,
ColorWritten = 1 << 9
};
typedef Containers::EnumSet<InternalFlag> InternalFlags;

Expand All @@ -775,7 +813,7 @@ class CORRADE_UTILITY_EXPORT Debug {
InternalFlags _flags;
InternalFlags _immediateFlags;

/* 2 / 6 bytes free */
/* 0 / 4 bytes free */

private:
#ifdef CORRADE_SOURCE_LOCATION_BUILTINS_SUPPORTED
Expand Down
95 changes: 91 additions & 4 deletions src/Corrade/Utility/Test/DebugTest.cpp
Expand Up @@ -31,6 +31,7 @@
#include <string>
#include <vector>

#include "Corrade/Containers/Pair.h"
#include "Corrade/Containers/String.h"
#include "Corrade/Containers/StringView.h"
#include "Corrade/TestSuite/Tester.h"
Expand Down Expand Up @@ -79,6 +80,8 @@ struct DebugTest: TestSuite::Tester {
void colorsNoOutput();
void colorsScoped();

void hex();

void valueAsColor();
void valueAsColorColorsDisabled();

Expand Down Expand Up @@ -152,6 +155,8 @@ DebugTest::DebugTest() {
&DebugTest::colorsNoOutput,
&DebugTest::colorsScoped,

&DebugTest::hex,

&DebugTest::valueAsColor,
&DebugTest::valueAsColorColorsDisabled,

Expand Down Expand Up @@ -809,6 +814,88 @@ void DebugTest::colorsScoped() {
#endif
}

void DebugTest::hex() {
/* Local hex modifier, applied once */
{
std::ostringstream out;

{
Debug d{&out};
d << "Values";
CORRADE_VERIFY(!(d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY(!(d.immediateFlags() & Debug::Flag::Hex));

d << Debug::hex;
CORRADE_VERIFY(!(d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY((d.immediateFlags() & Debug::Flag::Hex));

d << 0xc0ffee;
CORRADE_VERIFY(!(d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY(!(d.immediateFlags() & Debug::Flag::Hex));

d << "and" << 16;
}

CORRADE_COMPARE(out.str(), "Values 0xc0ffee and 16\n");
}

/* Global hex modifier, applied always */
{
std::ostringstream out;
{
Debug d{&out, Debug::Flag::Hex};
CORRADE_VERIFY((d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY((d.immediateFlags() & Debug::Flag::Hex));

/* Should work for any integer type without truncating, 0x should
be printed for 0 as well */
d << 0xfedcba9876543210ull << 0xcdu << static_cast<signed char>(0x13) << 0x0;
CORRADE_VERIFY((d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY((d.immediateFlags() & Debug::Flag::Hex));

/* Shouldn't be applied to non-integer types but should still stay
present for any that may come after */
d << "yes" << 3.5f << false << 0xabc << U'\xabc';
CORRADE_VERIFY((d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY((d.immediateFlags() & Debug::Flag::Hex));

/* Printing pointers applies it implicitly, check it doesn't
cause 0x to be printed twice or the flag reset after */
d << nullptr << reinterpret_cast<const void*>(std::size_t{0xc0ffee}) << 0x356;
CORRADE_VERIFY((d.flags() & Debug::Flag::Hex));
CORRADE_VERIFY((d.immediateFlags() & Debug::Flag::Hex));
}

CORRADE_COMPARE(out.str(),
"0xfedcba9876543210 0xcd 0x13 0x0 "
"yes 3.5 false 0xabc U+0ABC "
"nullptr 0xc0ffee 0x356\n");
}

/* Negative values should have - before the 0x. Well, ideally, if iostreams
weren't irreparably broken in the first place, printing everything as
unsigned. */
{
std::ostringstream out;
Debug{&out, Debug::Flag::Hex} << -0x356 << -0x1ll;

{
CORRADE_EXPECT_FAIL("This doesn't work as expected with std::hex anyway, won't bother fixing until iostreams are dropped.");
CORRADE_COMPARE(out.str(), "-0x356 -0x1\n");
}

CORRADE_COMPARE(out.str(), "0xfffffcaa 0xffffffffffffffff\n");
}

/* Nested values should be printed as hex too, but it should be reset
after */
{
std::ostringstream out;
Debug{&out} << Debug::hex << Containers::pair(0xab, Containers::arrayView({0xcd, 0x13})) << 1234;
CORRADE_COMPARE(out.str(), "{0xab, {0xcd, 0x13}} 1234\n");
}
}

void DebugTest::valueAsColor() {
Debug{} << "The following should be shades of gray:";

Expand Down Expand Up @@ -1066,15 +1153,15 @@ void DebugTest::debugColor() {
void DebugTest::debugFlag() {
std::ostringstream out;

Debug(&out) << Debug::Flag::NoNewlineAtTheEnd << Debug::Flag(0xde);
CORRADE_COMPARE(out.str(), "Utility::Debug::Flag::NoNewlineAtTheEnd Utility::Debug::Flag(0xde)\n");
Debug(&out) << Debug::Flag::NoNewlineAtTheEnd << Debug::Flag(0xcafe);
CORRADE_COMPARE(out.str(), "Utility::Debug::Flag::NoNewlineAtTheEnd Utility::Debug::Flag(0xcafe)\n");
}

void DebugTest::debugFlags() {
std::ostringstream out;

Debug(&out) << (Debug::Flag::NoNewlineAtTheEnd|Debug::Flag::Packed) << Debug::Flags{};
CORRADE_COMPARE(out.str(), "Utility::Debug::Flag::NoNewlineAtTheEnd|Utility::Debug::Flag::Packed Utility::Debug::Flags{}\n");
Debug(&out) << (Debug::Flag::NoNewlineAtTheEnd|Debug::Flag::Packed|Debug::Flag(0xb000)) << Debug::Flags{};
CORRADE_COMPARE(out.str(), "Utility::Debug::Flag::NoNewlineAtTheEnd|Utility::Debug::Flag::Packed|Utility::Debug::Flag(0xb000) Utility::Debug::Flags{}\n");
}

#ifndef CORRADE_TARGET_EMSCRIPTEN
Expand Down

0 comments on commit 718e5fd

Please sign in to comment.