xo-umbrella2/xo-ppsink/include/xo/ppsink/Prettifier.hpp
Roland Conybeare 0781131497
All checks were successful
cmake-docker / cmake-build (clang, clang++) (push) Successful in 47m28s
cmake-docker / cmake-build (gcc, g++) (push) Successful in 41m49s
CI / smoke-test (push) Successful in 58m44s
xo-ppsink: print boolean as true/false [FEATURE]
2026-09-08 20:18:54 -04:00

226 lines
7.6 KiB
C++

/** @file Prettifier.hpp
*
* @author Roland Conybeare, Jul 2026
*
* Prettifier<T>: the per-type customization *class* for structured printing.
* Specialize it to teach a type how to render into a PpSink. The dispatch
* verb that consults it, pretty(sink, x), lives in pretty.hpp.
**/
#pragma once
#include "PpSink.hpp"
#include <cassert> /* XO_PRETTIFIER_VIA_CONVERSION expands to assert(false) */
#include <charconv>
#include <cstdint>
#include <concepts>
#include <string_view>
#include <type_traits>
namespace xo::pp {
/** @brief per-type opt-in to structured pretty-printing.
*
* Specialize for a type @p T that should participate in pretty-printing,
* providing:
* @code
* static void print(PpSink & sink, const T & x);
* @endcode
*
* The primary template is empty (no @c print), so @ref has_prettifier is
* false for a type until @c Prettifier<T> is specialized. A type without a
* specialization renders as a leaf (see @ref pretty): string-like via
* PpSink::put, anything else via operator<<.
**/
template <typename T>
class Prettifier {
public:
/** identifies a type T for which Prettifier<T> has not been specialized **/
using pp_primary_template = void;
};
/** true iff @c Prettifier<T> supplies @c print(PpSink&, const T&) **/
template <typename T>
concept has_prettifier = requires (PpSink & sink, const T & x) {
Prettifier<T>::print(sink, x);
};
template <typename T>
concept prettifier_not_specialized = requires { typename Prettifier<T>::pp_primary_template; };
template <typename T>
concept prettifier_specialized = !prettifier_not_specialized<T>;
template <typename T>
concept prettifier_broken = prettifier_specialized<T> && !has_prettifier<T>;
# define XO_PRETTIFIER_DECLARE(T) \
template <> \
class Prettifier<T> { \
public: \
static void print(PpSink & sink, const T & x); \
};
/** non-inline. Intended for .cpp files **/
# define XO_PRETTIFIER_VIA_PRETTY_METHOD(T) \
void \
Prettifier<T>::print(PpSink & sink, const T & x) \
{ \
x.pretty(sink); \
}
/** non-inline. Intended for .cpp files **/
# define XO_PRETTIFIER_VIA_CONVERSION(T,FN) \
void \
Prettifier<T>::print(PpSink & sink, const T & x) \
{ \
auto _tmp = (FN)(x); \
if (_tmp) \
sink.put(_tmp); \
else \
assert(false); \
}
/** Integer types that should render as NUMBERS.
* Takes all integral types except for bool and char-oriented types
**/
template <typename T>
concept pp_number_integral
= std::integral<T>
&& !std::same_as<std::remove_cv_t<T>, bool>
&& !std::same_as<std::remove_cv_t<T>, char>
&& !std::same_as<std::remove_cv_t<T>, signed char>
&& !std::same_as<std::remove_cv_t<T>, unsigned char>
&& !std::same_as<std::remove_cv_t<T>, wchar_t>
&& !std::same_as<std::remove_cv_t<T>, char8_t>
&& !std::same_as<std::remove_cv_t<T>, char16_t>
&& !std::same_as<std::remove_cv_t<T>, char32_t>;
/** Use:
* PpSink sink = ...;
* using T = uint32_t; // or another integral type
* T x = 123;
* Prettifier<T>::print(sink, x);
* writes
* "123" to sink
**/
template <pp_number_integral T>
struct Prettifier<T> {
static void print(PpSink & sink, T x) {
char buf[24];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), x);
(void)ec; /* cannot fail: buf is large enough for any integer */
sink.put(std::string_view(buf,
static_cast<std::size_t>(ptr - buf)));
}
};
/** Use:
* PpSink sink = ...;
* Prettifier<bool>::print(sink, true)
* writes
* "true" to sink.
*
* Spelled out rather than 1/0: a bare 1 among neighbouring integer fields
* does not say which of them is a flag, and the digits are the one
* rendering that cannot be told apart from a count.
**/
template <>
struct Prettifier<bool> {
static void print(PpSink & sink, bool x) {
sink.put(x ? std::string_view("true") : std::string_view("false"));
}
};
/** default precision for floating-point leaves.
* Matching ostream's default as placeholder for something more elaborate.
**/
static constexpr int c_default_float_precision = 6;
/** Use:
* PpSink sink = ...;
* Prettifier<double>::print(sink, 0.1234)
* writes
* "0.1234" to sink.
*
* Equivalent to "%.6g" for now
**/
template <>
struct Prettifier<double> {
static void print(PpSink & sink, double x) {
/* "%.6g" is at most ~13 chars ("-1.23457e-308"); buf[32] is ample */
char buf[32];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), x,
std::chars_format::general,
c_default_float_precision);
if (ec != std::errc()) {
/* cannot happen with buf[32] at precision 6, but do not emit
* garbage if it ever does
*/
sink.put("<double?>");
return;
}
sink.put(std::string_view(buf,
static_cast<std::size_t>(ptr - buf)));
}
};
/** promote float -> double for printing.
* Consistent with operator<< policy
**/
template <>
struct Prettifier<float> {
static void print(PpSink & sink, float x) {
Prettifier<double>::print(sink, static_cast<double>(x));
}
};
namespace detail {
/** Use:
* PpSink sink = ...;
* const void *x = 0x1234;
* detail::pp_put_pointer(sink, x);
* writes
* "0x1234" to sink
*
* nullptr formatted as
* "null"
**/
inline void pp_put_pointer(PpSink & sink, const void * p) {
if (!p) {
sink.put(std::string_view("null"));
return;
}
/* "0x" + two hex digits per byte; +1 keeps to_chars' end in range */
char buf[2 + (2 * sizeof(void *)) + 1];
buf[0] = '0';
buf[1] = 'x';
auto [ptr, ec] = std::to_chars(buf + 2, buf + sizeof(buf),
reinterpret_cast<std::uintptr_t>(p),
16);
(void)ec; /* cannot fail: buf holds every pointer width */
sink.put(std::string_view(buf,
static_cast<std::size_t>(ptr - buf)));
}
}
template <>
struct Prettifier<void *> {
static void print(PpSink & sink, const void * x) {
detail::pp_put_pointer(sink, x);
}
};
template <>
struct Prettifier<const void *> {
static void print(PpSink & sink, const void * x) {
detail::pp_put_pointer(sink, x);
}
};
} /*namespace xo::pp*/
/* end Prettifier.hpp */