From 51424295ae9ea77486270d7eca5c64eb9ea0bd99 Mon Sep 17 00:00:00 2001 From: Markus Mittendrein Date: Thu, 6 Jan 2022 19:19:34 +0100 Subject: Move cxxformat include-directory into a separate include directory --- include/cxxformat/core.hpp | 572 +++++++++++++++++++++++++++++++++++++++ include/cxxformat/cxxformat | 7 + include/cxxformat/file_ptr.hpp | 23 ++ include/cxxformat/formatters.hpp | 486 +++++++++++++++++++++++++++++++++ include/cxxformat/helpers.hpp | 260 ++++++++++++++++++ include/cxxformat/ostream.hpp | 27 ++ include/cxxformat/string.hpp | 25 ++ 7 files changed, 1400 insertions(+) create mode 100644 include/cxxformat/core.hpp create mode 100644 include/cxxformat/cxxformat create mode 100644 include/cxxformat/file_ptr.hpp create mode 100644 include/cxxformat/formatters.hpp create mode 100644 include/cxxformat/helpers.hpp create mode 100644 include/cxxformat/ostream.hpp create mode 100644 include/cxxformat/string.hpp (limited to 'include') diff --git a/include/cxxformat/core.hpp b/include/cxxformat/core.hpp new file mode 100644 index 0000000..8669f1c --- /dev/null +++ b/include/cxxformat/core.hpp @@ -0,0 +1,572 @@ +#pragma once + +#include +#include +#include + +#include + +namespace format { + namespace { + template + struct str + { + public: + const char s[N + 1]; + + constexpr std::size_t size() const noexcept { return N; } + + constexpr str(const char (&s)[N + 1]) noexcept : str{s, std::make_index_sequence()} {} + constexpr str(std::string_view s) noexcept : str{s, std::make_index_sequence()} {} + + constexpr operator std::string_view() const noexcept + { + return {s, N}; + } + + constexpr bool operator==(std::string_view other) const noexcept + { + return operator std::string_view() == other; + } + + template + consteval auto substr(holder, holder) const noexcept + { + static_assert(pos + count <= N); + return str{s, pos, std::make_index_sequence()}; + } + + template + consteval auto substr(holder) const noexcept + { + return substr(holder{}, holder{}); + } + + template + consteval auto substr(holder p) const noexcept + { + return substr(p, holder{}); + } + + constexpr char operator[](std::size_t i) const noexcept + { + return s[i]; + } + + private: + template + constexpr str(const char (&s)[N + 1], std::index_sequence) noexcept : s{s[indices]...} {} + + template + constexpr str(const char (&s)[M], std::size_t offset, std::index_sequence) noexcept : s{s[indices + offset]..., '\0'} {} + + template + constexpr str(std::string_view s, std::index_sequence) noexcept : s{s[indices]..., '\0'} {} + + friend std::ostream& operator<<(std::ostream& out, const str& s) + { + return out << s.operator std::string_view(); + } + + template + friend struct str; + }; + + template + str(const char(&)[N]) -> str; + + template + struct FormatOutputBase { + constexpr void operator()(std::string_view what) const + { + this_().output(what); + } + + constexpr void operator()(std::string_view what, std::size_t count) const + { + for (std::size_t i = 0; i < count; ++i) + { + this_()(what); + } + } + + constexpr void operator()(char what) const + { + this_()({&what, 1}); + } + + constexpr void operator()(char what, std::size_t count) const + { + this_()({&what, 1}, count); + } + + private: + constexpr const FormatOutput& this_() const + { + static_assert(std::derived_from>, "FormatOutputBase must be used with CRTP"); + static_assert(requires(const FormatOutput o, std::string_view s) { o.output(s); }, "The derived class must implement the method output(std::string_view)"); + return static_cast(*this); + } + }; + + template + concept format_output = std::derived_from, FormatOutputBase>> && requires(const T o, std::string_view s) { o.output(s); }; + + struct NullOutput : FormatOutputBase { + constexpr void output(std::string_view) const noexcept {} + }; + + using ArgIndex = std::uint16_t; + struct format_specifier { + enum Length : std::uint8_t { + None, + Quarter, // hh + Half, // h + Long, // l + LongLong, // ll + LongDouble, // L + Maximum, // j + SizeT, // z + PtrDiff // t + }; + + optional_int minWidth; + optional_int precision; + ArgIndex argIndex; + char conversion; + char addSign; + char padding; + struct { + bool leftJustified : 1; + bool alternative : 1; + bool widthAsArg : 1; + bool precisionAsArg : 1; + Length length : 4; + }; + }; + } + + template + constexpr decltype(auto) make_output(Out&& out) { return std::forward(out); } + + template + concept format_output_compatible = format_output || requires(T&& o) { {make_output(std::forward(o))} -> format_output; }; + + template + struct formatter; + + template + concept has_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { + { formatter::format(out, t, spec, z, z) } -> std::same_as; + { formatter::template format(out, t, z, z) } -> std::same_as; + }; + + template + concept has_fallback_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { + { formatter::format(out, t, spec, z, z) } -> std::same_as; + { formatter::template format(out, t, z, z) } -> std::same_as; + }; + + template + concept has_some_formatter = has_formatter || has_fallback_formatter; + + namespace { + template + struct formatter_with_fallback : formatter {}; + + template requires (!has_formatter) + struct formatter_with_fallback : formatter {}; + + template + constexpr std::optional digit(char c) noexcept + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + return std::nullopt; + } + + template + constexpr std::pair, std::size_t> parseNumber(std::string_view s, std::size_t pos) noexcept + { + if (pos >= s.size()) + { + return {std::nullopt, pos}; + } + + bool isNegative = (std::signed_integral && s[pos] == '-'); + auto startPos = isNegative ? pos + 1 : pos; + if (startPos >= s.size()) + { + return {std::nullopt, pos}; + } + + auto d = digit(s[startPos]); + if (!d) + { + return {std::nullopt, pos}; + } + + T val{*d}; + auto p = startPos + 1; + for (; p < s.size(); ++p) + { + auto nextD = digit(s[p]); + if (!nextD) + { + break; + } + + val *= 10; + val += *nextD; + } + return {isNegative ? -val : val, p}; + } + + constexpr auto parseSpec(std::string_view fmt, std::size_t pos, ArgIndex nextArg) + { + struct Result { + std::size_t pos; + format_specifier spec; + ArgIndex nextArg; + ArgIndex maxArg; + }; + + const auto checkEos = [&pos, fmt](std::optional p = std::nullopt) { + if (p.value_or(pos) >= fmt.size()) + { + throw std::invalid_argument{"Incomplete format specifier at end of format string"}; + } + }; + + format_specifier spec{ + .minWidth = std::nullopt, + .precision = std::nullopt, + .addSign = '\0', + .padding = ' ', + }; + spec.leftJustified = false; + spec.alternative = false; + spec.widthAsArg = false; + spec.precisionAsArg = false; + spec.length = format_specifier::None; + + bool hasExplicitArgIndex = false; + const auto argIndex = parseNumber(fmt, pos); + if (argIndex.first) + { + checkEos(argIndex.second); + if (fmt[argIndex.second] == '$') + { + if (*argIndex.first == 0) + { + throw std::invalid_argument{"Placeholder indices are 1-based and can’t be 0"}; + } + spec.argIndex = *argIndex.first - 1; + hasExplicitArgIndex = true; + pos = argIndex.second + 1; + checkEos(); + } + } + + for (; pos < fmt.size(); ++pos) + { + const auto c = fmt[pos]; + if (c == '-') + { + spec.leftJustified = true; + } + else if (c == '+') + { + spec.addSign = '+'; + } + else if (c == ' ') + { + spec.addSign = ' '; + } + else if (c == '#') + { + spec.alternative = true; + } + else if (c == '0') + { + spec.padding = '0'; + } + else + { + break; + } + } + if (spec.leftJustified) + { + spec.padding = ' '; + } + checkEos(); + + ArgIndex maxArg = 0; + const auto argIndexOrNextArg = [&pos, &nextArg, &maxArg, checkEos, fmt](decltype(*format_specifier::minWidth)& argIndex) { + const auto index = parseNumber(fmt, pos); + checkEos(index.second); + if (index.first && fmt[index.second] == '$') + { + if (*index.first == 0) + { + throw std::invalid_argument{"Placeholder indices are 1-based and can’t be 0"}; + } + argIndex = *index.first - 1; + pos = index.second + 1; + checkEos(); + } + else + { + argIndex = nextArg; + ++nextArg; + } + maxArg = std::max(maxArg, argIndex); + }; + + const auto handleWidthPrecision = [&pos, argIndexOrNextArg, fmt, checkEos](decltype(format_specifier::minWidth)& indexOrValue, bool allowEmpty = false){ + bool asArg = false; + if (fmt[pos] == '*') + { + asArg = true; + ++pos; + checkEos(); + argIndexOrNextArg(indexOrValue.emplace()); + } + else + { + const auto width = parseNumber(fmt, pos); + if (width.first) + { + indexOrValue = *width.first; + pos = width.second; + } + else if (allowEmpty) + { + indexOrValue = 0; + } + } + return asArg; + }; + spec.widthAsArg = handleWidthPrecision(spec.minWidth); + + + if (fmt[pos] == '.') + { + ++pos; + checkEos(); + spec.precisionAsArg = handleWidthPrecision(spec.precision, true); + } + + { + using L = format_specifier::Length; + const auto c = fmt[pos]; + if (c == 'h' || c == 'l') + { + if (fmt[pos + 1] == c) + { + ++pos; + checkEos(); + spec.length = (c == 'h' ? L::Quarter : L::LongLong); + ++pos; + } + else + { + spec.length = (c == 'h' ? L::Half : L::Long); + ++pos; + } + } + else if (c == 'L') + { + spec.length = L::LongDouble; + ++pos; + } + else if (c == 'j') + { + spec.length = L::Maximum; + ++pos; + } + else if (c == 'z') + { + spec.length = L::SizeT; + ++pos; + } + else if (c == 't') + { + spec.length = L::PtrDiff; + ++pos; + } + } + checkEos(); + + spec.conversion = fmt[pos]; + ++pos; + + if (!hasExplicitArgIndex) + { + spec.argIndex = nextArg; + ++nextArg; + } + maxArg = std::max(maxArg, spec.argIndex); + + return Result{pos, spec, nextArg, static_cast(maxArg + 1)}; + } + + constexpr bool checkValidConversion(char c) noexcept + { + return std::string_view{"diouxXeEfFgGaAcspv"}.find(c) != std::string_view::npos; + } + + namespace compile_time { + template + struct is_str_s : std::false_type {}; + + template + struct is_str_s> : std::true_type {}; + + template + concept is_str = is_str_s::value; + + template + concept template_part = std::same_as || is_str; + + template + consteval void assertArgumentUsed() + { + static_assert(used, "Argument is not used (1-based argIndex)"); + } + + template + struct format_template { + template requires (has_some_formatter> && ...) + constexpr void operator()(Output&& output, Args&&... args) const + { + checkArgCount(); + + const auto out = make_output(std::forward(output)); + const auto format_arg = [&args..., &out](holder) -> decltype(auto) + { + optional_int minWidth = spec.minWidth; + if constexpr (spec.widthAsArg) + { + using Arg = std::decay_t>; + static_assert(std::unsigned_integral, "Width argument must be an unsigned integral"); + static_assert(sizeof(Arg) <= sizeof(std::size_t), "Width argument type must be at most as big as std::size_t"); + minWidth = nth_argument<*spec.minWidth>(std::forward(args)...); + } + optional_int precision = spec.precision; + if constexpr (spec.precisionAsArg) + { + using Arg = std::decay_t>; + static_assert(std::unsigned_integral, "Precision argument must be an unsigned integral"); + static_assert(sizeof(Arg) <= sizeof(std::size_t), "Precision argument type must be at most as big as std::size_t"); + precision = nth_argument<*spec.precision>(std::forward(args)...); + } + auto&& arg = nth_argument(std::forward(args)...); + return formatter_with_fallback>::template format(out, arg, minWidth, precision); + }; + + (([&]{ + if constexpr (is_str>) + { + out(parts); + } + else + { + format_arg(holder{}); + } + })(), ...); + } + + private: + template + consteval static void checkArgCount() + { + if constexpr (i != argCount) + { + static_assert(i > argCount, "Not enough arguments passed."); + static_assert(i < argCount, "Too many arguments passed."); + } + } + + consteval static void checkArgsUsed() + { + checkArgsUsed(std::make_index_sequence()); + } + + template + consteval static void checkArgsUsed(std::index_sequence) + { + (checkArgUsed(), ...); + } + + template + consteval static void checkArgUsed() + { + assertArgumentUsed>) + { + return false; + } + else + { + return parts.argIndex == i || (parts.widthAsArg && parts.minWidth == i) || (parts.precisionAsArg && parts.precision == i); + } + }() || ...)>(); + } + + static_assert((checkArgsUsed(), true)); + }; + + template + consteval auto parseSpec() noexcept + { + constexpr auto specResult = parseSpec(fmt, pos, nextArg); + static_assert(checkValidConversion(specResult.spec.conversion), "Invalid conversion specifier"); + return specResult; + } + + template + consteval auto parseFormat() noexcept + { + if constexpr (pos >= fmt.size()) + { + return format_template{}; + } + else + { + constexpr auto nextSpec = std::string_view{fmt}.find('%', pos); + if constexpr (nextSpec == std::string_view::npos) + { + return parseFormat{})>(); + } + else + { + static_assert(nextSpec + 1 < fmt.size(), "Incomplete format specifier at end of format string"); + + constexpr auto specStart = fmt[nextSpec + 1]; + if constexpr (specStart == '%') + { + return parseFormat{}), str{"%"}>(); + } + else + { + constexpr auto result = parseSpec(); + return parseFormat{}), result.spec>(); + } + } + } + } + + template + consteval auto operator ""_format_to() noexcept + { + return parseFormat(); + } + } + } + + using compile_time::operator ""_format_to; +} diff --git a/include/cxxformat/cxxformat b/include/cxxformat/cxxformat new file mode 100644 index 0000000..6da253a --- /dev/null +++ b/include/cxxformat/cxxformat @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include +#include +#include diff --git a/include/cxxformat/file_ptr.hpp b/include/cxxformat/file_ptr.hpp new file mode 100644 index 0000000..da516b0 --- /dev/null +++ b/include/cxxformat/file_ptr.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +namespace format { + namespace { + class FilePtrOutput : public FormatOutputBase { + FILE* to; + + public: + FilePtrOutput(FILE* to) noexcept : to{to} {} + + void output(std::string_view what) const + { + fwrite(what.data(), what.size(), 1, to); + } + }; + } + +} +[[maybe_unused]] format::FilePtrOutput make_output(FILE* to) noexcept { return {to}; } diff --git a/include/cxxformat/formatters.hpp b/include/cxxformat/formatters.hpp new file mode 100644 index 0000000..a5c92ed --- /dev/null +++ b/include/cxxformat/formatters.hpp @@ -0,0 +1,486 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace format { + namespace { + constexpr void formatPadded(const format_output auto& out, std::string_view subject, char padding, bool leftJustified, optional_int minWidth) + { + if(subject.length() < minWidth.value_or(0) && !leftJustified) + { + out(padding, *minWidth - subject.length()); + } + + out(subject); + + if(subject.length() < minWidth.value_or(0) && leftJustified) + { + out(padding, *minWidth - subject.length()); + } + } + } + + using invalid_argument = std::invalid_argument; + + template + struct formatter { + static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) + { + int base = 10; + switch(spec.conversion) + { + case 'v': + break; + case 'c': + { + // TODO: see static_assert version + char converted = static_cast(static_cast(t)); + formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth); + return; + } + case 'i': case 'd': + if constexpr (std::unsigned_integral) + { + throw invalid_argument{"%i and %d are not supported for unsigned integrals"}; + } + break; + case 'o': + base = 8; + if constexpr (std::signed_integral) + { + throw invalid_argument{"%o is not supported for signed integrals"}; + } + break; + case 'x': case 'X': + base = 16; + if constexpr (std::signed_integral) + { + throw invalid_argument{"%x and %X are not supported for signed integrals"}; + } + break; + case 'u': + if constexpr (std::signed_integral) + { + throw invalid_argument{"%u is not supported for signed integrals"}; + } + break; + default: + throw invalid_argument{std::string{"The given integral type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; + } + + std::string_view converted; + std::array::digits + 2) / 3> result; + if (precision != 0 || t != 0) + { + const auto [end, ec] = std::to_chars(result.data(), result.data() + result.size(), t, base); + assert(ec == std::errc{}); + converted = {result.data(), end}; + } + const bool hasMinusSign = !std::unsigned_integral && converted.starts_with('-'); + if (hasMinusSign) + { + converted.remove_prefix(1); + } + + if (spec.conversion == 'X') + { + std::transform(result.begin(), result.end(), result.data(), [](char c) + { + return (c >= 'a' && c <= 'z') ? c + ('A' - 'a') : c; + }); + } + + char signPrefix{'\0'}; + std::string_view prefix; + if (hasMinusSign) + { + prefix = "-"; + } + else if (base != 10 && spec.alternative) + { + if (spec.conversion == 'o') + { + prefix = "0"; + } + else if (precision != 0 || t != 0) + { + if (spec.conversion == 'x') + { + prefix = "0x"; + } + else if (spec.conversion == 'X') + { + prefix = "0X"; + } + } + } + else if (spec.addSign != '\0') + { + signPrefix = spec.addSign; + prefix = {&signPrefix, 1}; + } + + std::size_t precisionPadding{0}; + std::size_t normalPadding{0}; + if (precision) + { + if (*precision > converted.size()) + { + precisionPadding = *precision - converted.size(); + } + } + else if (minWidth && (spec.alternative && base != 10 && spec.padding == '0')) + { + const auto totalLength = converted.size() + prefix.size(); + if (*minWidth > totalLength) + { + precisionPadding = *minWidth - totalLength; + minWidth.clear(); + } + } + + if (minWidth) + { + const auto totalLength = converted.size() + precisionPadding + prefix.size(); + if (*minWidth > totalLength) + { + normalPadding = *minWidth - totalLength; + } + } + + const auto padding = precision ? ' ' : spec.padding; + if(!spec.leftJustified) + { + out(padding, normalPadding); + } + + out(prefix); + out('0', precisionPadding); + + out(converted); + + if(spec.leftJustified) + { + out(padding, normalPadding); + } + return; + } + + template + static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) + { + constexpr auto conv = spec.conversion; + if constexpr (conv == 'c') + { + // TODO: what is the right thing to do here? + // static_assert(std::same_as, "%c only accepts int as argument"); + } + else if constexpr (conv == 'i' || conv == 'd') + { + static_assert(std::signed_integral, "%i and %d only accept signed integral types"); + } + else if constexpr (conv == 'o' || conv == 'x' || conv == 'X' || conv == 'u') + { + static_assert(std::unsigned_integral, "%o, %x, %X and %u only accept unsigned integral types"); + } + else if constexpr (conv != 'v') + { + static_assert(sizeof(T) == 0, "Unsupported conversion specified"); + } + + static_assert(spec.addSign == '\0' || conv == 'i' || conv == 'd', "‘+’ and ‘ ’ (sign related) flags are only allowed for %i and %d"); + static_assert(!spec.alternative || conv == 'o' || conv == 'x' || conv == 'X', "‘#’ (alternative) flag is only allowed for %x, %X and %o"); + + return format(out, t, spec, minWidth, precision); + } + }; + + // NOTE: Difference to printf: uses std::to_chars’ minimal width for exact representation when no precision is specified instead of default precision of 6 + template + struct formatter { + static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) + { + std::chars_format format; + bool uppercase = false; + switch(spec.conversion) + { + case 'A': + uppercase = true; + [[fallthrough]]; + case 'a': + format = std::chars_format::hex; + break; + case 'E': + uppercase = true; + [[fallthrough]]; + case 'e': + format = std::chars_format::scientific; + break; + case 'F': + uppercase = true; + [[fallthrough]]; + case 'f': + format = std::chars_format::fixed; + break; + case 'G': + uppercase = true; + [[fallthrough]]; + case 'g': + case 'v': + format = std::chars_format::general; + break; + default: + throw invalid_argument{std::string{"The given floating point type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; + } + + std::array::digits10 * 2 + 6)> result; + const auto [end, ec] = precision ? std::to_chars(result.data(), result.data() + result.size(), t, format ,*precision) : std::to_chars(result.data(), result.data() + result.size(), t, format); + assert(ec == std::errc{}); + std::string_view converted{result.data(), end}; + + const bool hasMinusSign = converted.starts_with('-'); + if (hasMinusSign) + { + converted.remove_prefix(1); + } + + std::string_view mantissa = converted; + std::string_view exponent; + if (const auto exponentPos = converted.find(format == std::chars_format::hex ? 'p' : 'e'); exponentPos != std::string_view::npos) + { + mantissa = converted.substr(0, exponentPos); + exponent = converted.substr(exponentPos); + } + + const bool hasDot = mantissa.find('.') != std::string_view::npos; + const bool addDot = spec.alternative && !hasDot; + + std::size_t extraZeros{0}; + if (spec.alternative && format == std::chars_format::general && precision) + { + const auto currentMantissaPrecision = mantissa.size() - (hasDot ? 1 : 0); + if (*precision > currentMantissaPrecision) + { + extraZeros = *precision - currentMantissaPrecision; + } + } + + if (uppercase) + { + std::transform(result.data(), end, result.data(), [](char c) + { + return (c >= 'a' && c <= 'z') ? c + ('A' - 'a') : c; + }); + } + + const char signPrefix = hasMinusSign ? '-' : spec.addSign; + std::string_view hexPrefix; + if (format == std::chars_format::hex && std::isfinite(t)) + { + if (spec.conversion == 'A') + { + hexPrefix = "0X"; + } + else + { + hexPrefix = "0x"; + } + } + + std::size_t paddingLength{0}; + if (minWidth) + { + const auto totalLength = (signPrefix != '\0' ? 1 : 0) + hexPrefix.size() + converted.size() + (addDot ? 1 : 0) + extraZeros; + if (*minWidth > totalLength) + { + paddingLength = *minWidth - totalLength; + } + } + + if(!spec.leftJustified && spec.padding != '0') + { + out(spec.padding, paddingLength); + } + + if (signPrefix != '\0') + { + out(signPrefix); + } + out(hexPrefix); + + if(!spec.leftJustified && spec.padding == '0') + { + out(spec.padding, paddingLength); + } + + out(mantissa); + if (addDot) + { + out('.'); + } + out('0', extraZeros); + out(exponent); + + if(spec.leftJustified) + { + out(spec.padding, paddingLength); + } + return; + } + + template + static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) + { + static_assert(std::string_view{"fFeEaAgGv"}.find(spec.conversion) != std::string_view::npos, "Unsupported conversion specified for floating point type"); + return format(out, t, spec, minWidth, precision); + } + }; + + template + concept stringy = requires(T t) { std::string_view{t}; }; + + template + struct formatter { + static constexpr void format(const format_output auto& out, const S& s, format_specifier spec, optional_int minWidth, optional_int precision) + { + if (spec.conversion != 's' && spec.conversion != 'v') + { + throw invalid_argument{"The given stringy type only supports the ‘s’ and ‘v’ conversions"}; + } + std::string_view val(s); + if (precision) + { + val = val.substr(0, *precision); + } + + formatPadded(out, val, ' ', spec.leftJustified, minWidth); + } + + template + static constexpr void format(const format_output auto& out, const S& s, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 's' || spec.conversion == 'v', "The given stringy type only supports the ‘s’ and ‘v’ conversions"); + return format(out, s, spec, minWidth, precision); + } + }; + + template + concept pointer = std::is_pointer_v; + + template + struct formatter { + static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, format_specifier spec, optional_int minWidth, optional_int precision) + { + if (t == nullptr) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + else + { + constexpr format_specifier spec{.conversion = 'x', .alternative = true}; + formatter::template format(out, std::bit_cast(t), minWidth, std::nullopt); + } + } + + template + static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); + return format(out, t, spec, minWidth, precision); + } + }; + + template<> + struct formatter { + static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int minWidth, optional_int precision) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + + template + static constexpr void format(const format_output auto& out, std::nullptr_t, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); + return format(out, nullptr, spec, minWidth, precision); + } + }; + + template<> + struct formatter { + static constexpr void format(const format_output auto& out, const char* s, format_specifier spec, optional_int minWidth, optional_int precision) + { + if (spec.conversion == 'p') + { + formatter::format(out, static_cast(s), spec, minWidth, precision); + } + else + { + assert(spec.conversion == 's' || spec.alternative == 'v'); + + if (s == nullptr) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + else + { + formatter::format(out, s, spec, minWidth, precision); + } + } + } + + template + static constexpr void format(const format_output auto& out, const char* s, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'p' || spec.conversion == 's' || spec.conversion == 'v', "const char* only supports the ‘p’, ‘s’ and ‘v’ conversions"); + return format(out, s, spec, minWidth, precision); + } + }; + + template<> + struct formatter : formatter {}; + + template + concept ostreamable = requires(T t, std::ostream& out) { {out << t} -> std::same_as; }; + + template + concept has_ostream = requires(T t) { {t.stream()} -> std::same_as; }; + + template + struct formatter { + static void format(const format_output auto& out, const T& t, format_specifier spec, optional_int minWidth, optional_int) + { + if constexpr (has_ostream) + { + if (!minWidth) + { + out.stream() << t; + return; + } + } + + std::ostringstream stream; + stream << t; + formatPadded(out, stream.view(), ' ', spec.leftJustified, minWidth); + } + + template + static constexpr void format(const format_output auto& out, const T& t, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'v' || spec.conversion == 's', "The operator<<(std::ostream&) fallback only supports conversions ‘s’ and ‘v’"); + static_assert(!spec.precision, "The operator<<(std::ostream&) fallback does not support specifying an precision"); + static_assert(spec.addSign == '\0', "The operator<<(std::ostream&) fallback does not support the add sign (‘+‘) flag"); + static_assert(!spec.alternative, "The operator<<(std::ostream&) fallback does not support the alternative (‘#‘) flag"); + static_assert(spec.padding == ' ', "The operator<<(std::ostream&) fallback does not support padding with 0"); + static_assert(spec.length == format_specifier::Length::None, "The operator<<(std::ostream&) fallback does not support specifying any length"); + return format(out, t, spec, minWidth, precision); + } + }; +} diff --git a/include/cxxformat/helpers.hpp b/include/cxxformat/helpers.hpp new file mode 100644 index 0000000..73cb0be --- /dev/null +++ b/include/cxxformat/helpers.hpp @@ -0,0 +1,260 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace detail { + template + class FunctorHelper : T { + using T::operator(); + }; +} + +template +concept functor = requires { + std::declval>(); +}; + +template +class overloaded_callable : public T... +{ +public: + overloaded_callable(T&&... bases) : T{std::forward(bases)}... {} + using T::operator()...; +}; + +template +struct nth_argument_s +{ + template + constexpr decltype(auto) operator()(Arg&& arg, Args&&... args) const noexcept + { + static_assert(i < sizeof...(Args) + 1, "index must be smaller than the number of arguments"); + if constexpr (i == 0) + { + return std::forward(arg); + } + else + { + return nth_argument_s{}(std::forward(args)...); + } + } +}; + +template +constexpr inline auto nth_argument = nth_argument_s{}; + +template +struct nth_type_s +{ + static_assert(i < sizeof...(Args) + 1, "index must be smaller than the number of arguments"); + using type = typename nth_type_s::type; +}; + +template +struct nth_type_s<0, Arg, Args...> +{ + using type = Arg; +}; + +template +using nth_type = typename nth_type_s::type; + +template +struct holder {}; + +namespace helper_concepts { +template class optional> +concept optional_ctor_ok = requires { + !std::is_constructible_v&>; + !std::is_constructible_v&>; + !std::is_constructible_v&&>; + !std::is_constructible_v&&>; + !std::is_convertible_v&, T>; + !std::is_convertible_v&, T>; + !std::is_convertible_v&&, T>; + !std::is_convertible_v&&, T>; +}; +} + +template +struct structural_optional { + + bool hasValue; + union { + char empty; // structural types can not have uninitialized memory + T value; + }; + + constexpr structural_optional() noexcept : hasValue{false}, empty{} {} + constexpr structural_optional(std::nullopt_t) noexcept : hasValue{false}, empty{} {} + constexpr structural_optional(const T& value) noexcept(std::is_nothrow_copy_constructible_v) requires(std::copy_constructible) : hasValue{true}, value{value} {} + constexpr structural_optional(T&& value) noexcept(std::is_nothrow_move_constructible_v) requires(std::move_constructible) : hasValue{true}, value{std::move(value)} {} + template + constexpr structural_optional(std::in_place_t, Args&&... value) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v) : hasValue{true}, value{std::forward(value)...} {} + template + constexpr structural_optional(U&& value) noexcept(std::is_nothrow_convertible_v) requires(std::is_convertible_v) : hasValue{true}, value{std::forward(value)} {} + template + constexpr structural_optional(const structural_optional& other) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v && helper_concepts::optional_ctor_ok) : hasValue{other}, empty{} + { + if (hasValue) + { + std::construct_at(&value, *other); + } + } + template + constexpr structural_optional(structural_optional&& other) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v && helper_concepts::optional_ctor_ok) : hasValue{other}, empty{} + { + if (hasValue) + { + std::construct_at(&value, std::move(*other)); + } + } + + constexpr structural_optional(const structural_optional& other) noexcept(std::is_nothrow_copy_constructible_v) requires(std::copy_constructible) : hasValue{other.hasValue}, empty{} + { + if (hasValue) + { + std::construct_at(&value, other.value); + } + } + constexpr structural_optional(structural_optional&& other) noexcept(std::is_nothrow_move_constructible_v) requires(std::move_constructible) : hasValue{other.hasValue}, empty{} + { + if (hasValue) + { + std::construct_at(&value, std::move(other.value)); + } + } + + constexpr void clear() + { + if (hasValue) + { + value.~T(); + hasValue = false; + empty = {}; + } + } + + explicit constexpr operator bool() const noexcept { return hasValue; } + constexpr T& operator*() noexcept + { + assert(hasValue); + return value; + } + + constexpr const T& operator*() const noexcept + { + assert(hasValue); + return value; + } + + constexpr structural_optional& operator=(const T& val) noexcept(std::is_nothrow_copy_assignable_v) requires(std::assignable_from) + { + emplace(val); + return *this; + } + + constexpr structural_optional& operator=(T&& val) noexcept(std::is_nothrow_move_constructible_v) requires(std::assignable_from) + { + emplace(std::move(val)); + return *this; + } + + constexpr structural_optional& operator=(const structural_optional& other) noexcept(std::is_nothrow_copy_assignable_v) requires(std::assignable_from) + { + if (other.hasValue) + { + emplace(other.value); + } + else + { + clear(); + } + return *this; + } + + constexpr structural_optional& operator=(structural_optional&& other) noexcept(std::is_nothrow_move_assignable_v) requires(std::assignable_from) + { + if (other.hasValue) + { + emplace(std::move(other.value)); + } + else + { + clear(); + } + return *this; + } + + + constexpr const T& value_or(const T& fallback) const noexcept { return hasValue ? value : fallback; } + + constexpr bool operator==(const structural_optional& other) const + { + return hasValue == other.hasValue && (!hasValue || value == other.value); + } + + constexpr bool operator==(const T& other) const + { + return hasValue && value == other; + } + + template + constexpr T& emplace(Args&&... args) noexcept + { + clear(); + hasValue = true; + std::construct_at(&value, std::forward(args)...); + return value; + } + + constexpr ~structural_optional() + { + clear(); + } +}; + +template +struct optional_int { + T val; + + constexpr optional_int() noexcept : val{emptyVal} {} + constexpr optional_int(std::nullopt_t) noexcept : val{emptyVal} {} + constexpr optional_int(T val) noexcept : val{val} {} + constexpr optional_int(const optional_int&) noexcept = default; + template U> + constexpr optional_int(const optional_int& other) noexcept : val{other ? other.val : emptyVal} {} + constexpr optional_int& operator=(const optional_int&) noexcept = default; + template U> + constexpr optional_int& operator=(optional_int other) noexcept + { + val = other ? other.val : emptyVal; + return *this; + } + + static constexpr inline T emptyVal{std::signed_integral ? std::numeric_limits::min() : std::numeric_limits::max()}; + + constexpr void clear() noexcept + { + val = emptyVal; + } + + constexpr explicit operator bool() const noexcept { return val != emptyVal; } + constexpr bool empty() const noexcept { return !*this; } + + constexpr bool operator==(const optional_int& other) const noexcept = default; + constexpr bool operator==(T other) const noexcept + { + return !empty() && val == other; + } + constexpr T& emplace() noexcept { return val = 0; } + constexpr T& operator*() noexcept { return val; } + constexpr T operator*() const noexcept { return val; } + constexpr T value_or(T fallback) const noexcept { return empty() ? fallback : val; } +}; diff --git a/include/cxxformat/ostream.hpp b/include/cxxformat/ostream.hpp new file mode 100644 index 0000000..308b670 --- /dev/null +++ b/include/cxxformat/ostream.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +namespace format { + namespace { + class OstreamOutput : public FormatOutputBase { + std::ostream& to; + + public: + OstreamOutput(std::ostream& to) noexcept : to{to} {} + + void output(std::string_view what) const + { + to << what; + } + + std::ostream& stream() const noexcept { return to; } + }; + } + +} +namespace std { + [[maybe_unused]] format::OstreamOutput make_output(std::ostream& to) noexcept { return {to}; } +} diff --git a/include/cxxformat/string.hpp b/include/cxxformat/string.hpp new file mode 100644 index 0000000..06cb620 --- /dev/null +++ b/include/cxxformat/string.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include + +namespace format { + namespace { + namespace compile_time { + template + consteval auto operator ""_format() noexcept + { + return [formatter = parseFormat()](Args&&... args) -> std::string + { + std::ostringstream out; + formatter(out, std::forward(args)...); + return out.str(); + }; + } + } + } + + using compile_time::operator ""_format; +} -- cgit v1.2.3-54-g00ecf