summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarkus Mittendrein <maxmitti@maxmitti.tk>2022-01-07 18:40:42 +0100
committerMarkus Mittendrein <maxmitti@maxmitti.tk>2022-01-07 22:50:08 +0100
commitcb71e001e5088e9006c00634ce833f5659af855c (patch)
tree1bb004bdbbae91442437ca0e93104f17494cc2af
parent4f2eb7c1dfe467aa2d7a6652a111edc74aa7b0a1 (diff)
downloadcxxformat-cb71e001e5088e9006c00634ce833f5659af855c.tar.gz
cxxformat-cb71e001e5088e9006c00634ce833f5659af855c.zip
Add runtime implementation and refactor format_spec checks in formatters
-rw-r--r--CMakeLists.txt1
-rw-r--r--include/cxxformat/core.hpp27
-rw-r--r--include/cxxformat/cxxformat1
-rw-r--r--include/cxxformat/formatters.hpp247
-rw-r--r--include/cxxformat/runtime.hpp219
-rw-r--r--main.cpp13
6 files changed, 396 insertions, 112 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4e0e9cb..46720d7 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,6 +11,7 @@ add_library(cxxformat INTERFACE
include/cxxformat/formatters.hpp
include/cxxformat/helpers.hpp
include/cxxformat/ostream.hpp
+ include/cxxformat/runtime.hpp
include/cxxformat/string.hpp
)
target_include_directories(cxxformat INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
diff --git a/include/cxxformat/core.hpp b/include/cxxformat/core.hpp
index 8669f1c..993060d 100644
--- a/include/cxxformat/core.hpp
+++ b/include/cxxformat/core.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include <algorithm>
#include <optional>
#include <stdexcept>
#include <string_view>
@@ -155,17 +156,17 @@ namespace format {
template<typename T, bool fallback = false>
struct formatter;
- template<typename T>
- concept has_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int<std::size_t> z) {
- { formatter<T>::format(out, t, spec, z, z) } -> std::same_as<void>;
- { formatter<T>::template format<format_specifier{}>(out, t, z, z) } -> std::same_as<void>;
+ template<typename Formatter, typename Value>
+ concept is_formatter = requires(Value t, format_specifier spec, const NullOutput& out, optional_int<std::size_t> z) {
+ { Formatter::format(out, t, spec, z, z) } -> std::same_as<void>;
+ { Formatter::conversionSupported(spec) } -> std::same_as<void>;
};
template<typename T>
- concept has_fallback_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int<std::size_t> z) {
- { formatter<T, true>::format(out, t, spec, z, z) } -> std::same_as<void>;
- { formatter<T, true>::template format<format_specifier{}>(out, t, z, z) } -> std::same_as<void>;
- };
+ concept has_formatter = is_formatter<formatter<T>, T>;
+
+ template<typename T>
+ concept has_fallback_formatter = is_formatter<formatter<T, true>, T>;
template<typename T>
concept has_some_formatter = has_formatter<T> || has_fallback_formatter<T>;
@@ -464,8 +465,16 @@ namespace format {
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>(args)...);
}
+
auto&& arg = nth_argument<spec.argIndex>(std::forward<Args>(args)...);
- return formatter_with_fallback<std::decay_t<decltype(arg)>>::template format<spec>(out, arg, minWidth, precision);
+ using formatter = formatter_with_fallback<std::decay_t<decltype(arg)>>;
+
+ []() consteval {
+ // throws on error
+ formatter::conversionSupported(spec);
+ }();
+
+ return formatter::format(out, arg, spec, minWidth, precision);
};
(([&]{
diff --git a/include/cxxformat/cxxformat b/include/cxxformat/cxxformat
index 6da253a..72bef6f 100644
--- a/include/cxxformat/cxxformat
+++ b/include/cxxformat/cxxformat
@@ -4,4 +4,5 @@
#include <cxxformat/formatters.hpp>
#include <cxxformat/file_ptr.hpp>
#include <cxxformat/ostream.hpp>
+#include <cxxformat/runtime.hpp>
#include <cxxformat/string.hpp>
diff --git a/include/cxxformat/formatters.hpp b/include/cxxformat/formatters.hpp
index a713018..ea0c08b 100644
--- a/include/cxxformat/formatters.hpp
+++ b/include/cxxformat/formatters.hpp
@@ -9,10 +9,18 @@
#include <charconv>
#include <concepts>
#include <limits>
+#include <memory>
#include <sstream>
+#include <system_error>
namespace format {
namespace {
+ template<str what>
+ constexpr auto operator ""_contains() noexcept
+ {
+ return [](char c) constexpr noexcept { return std::string_view{what}.find(c) != std::string_view::npos; };
+ }
+
constexpr void formatPadded(const format_output auto& out, std::string_view subject, char padding, bool leftJustified, optional_int<std::size_t> minWidth)
{
if(subject.length() < minWidth.value_or(0) && !leftJustified)
@@ -27,6 +35,51 @@ namespace format {
out(padding, *minWidth - subject.length());
}
}
+
+ constexpr void simpleConversionSpecifierCheck(std::string_view supported, char c, std::string_view typeDesc)
+ {
+ using namespace std::string_literals;
+ if (supported.find(c) == std::string_view::npos)
+ {
+ throw std::invalid_argument{"Unsupported conversion specifier ‘"s + c + "’ for "s + std::string{typeDesc} + "; Supported are "s + std::string{supported}};
+ }
+ }
+
+ constexpr void noAlternative(bool alternative, std::string_view typeDesc)
+ {
+ using namespace std::string_literals;
+ if (alternative)
+ {
+ throw std::invalid_argument{"‘#’ (alternative) flag is not supported for "s + std::string{typeDesc}};
+ }
+ }
+
+ constexpr void noSignFlags(char addSign, std::string_view typeDesc)
+ {
+ using namespace std::string_literals;
+ if (addSign != '\0')
+ {
+ throw std::invalid_argument{"‘+’ and ‘ ’ (sign related) flags are not supported for %s"s + std::string{typeDesc}};
+ }
+ }
+
+ constexpr void noZeroPadding(char padding, std::string_view typeDesc)
+ {
+ using namespace std::string_literals;
+ if (padding == '0')
+ {
+ throw std::invalid_argument{"Zero padding (‘0’ flag) is not supported for %s"s + std::string{typeDesc}};
+ }
+ }
+
+ constexpr void noPrecision(optional_int<std::size_t> precision, std::string_view typeDesc)
+ {
+ using namespace std::string_literals;
+ if (precision)
+ {
+ throw std::invalid_argument{"Specifying a precision is not supported for "s + std::string{typeDesc}};
+ }
+ }
}
using invalid_argument = std::invalid_argument;
@@ -35,54 +88,25 @@ namespace format {
struct formatter<T> {
static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
{
- int base = 10;
- switch(spec.conversion)
+ const auto conv = spec.conversion;
+ if (conv == 'c')
{
- case 'v':
- break;
- case 'c':
- {
- // TODO: see static_assert version
- char converted = static_cast<char>(static_cast<unsigned char>(t));
- formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth);
- return;
- }
- case 'i': case 'd':
- if constexpr (std::unsigned_integral<T>)
- {
- throw invalid_argument{"%i and %d are not supported for unsigned integrals"};
- }
- break;
- case 'o':
- base = 8;
- if constexpr (std::signed_integral<T>)
- {
- throw invalid_argument{"%o is not supported for signed integrals"};
- }
- break;
- case 'x': case 'X':
- base = 16;
- if constexpr (std::signed_integral<T>)
- {
- throw invalid_argument{"%x and %X are not supported for signed integrals"};
- }
- break;
- case 'u':
- if constexpr (std::signed_integral<T>)
- {
- 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{"’"}};
+ char converted = static_cast<char>(static_cast<unsigned char>(t));
+ formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth);
+ return;
}
+ const int base = std::unsigned_integral<T> ? (conv == 'o' ? 8 : "xX"_contains(conv) ? 16 : 10) : 10;
+
std::string_view converted;
std::array<char, (std::numeric_limits<T>::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{});
+ if (ec != std::errc{})
+ {
+ throw std::runtime_error{"Unexpected error from to_chars: " + std::make_error_condition(ec).message()};
+ }
converted = {result.data(), end};
}
const bool hasMinusSign = !std::unsigned_integral<T> && converted.starts_with('-');
@@ -175,77 +199,96 @@ namespace format {
return;
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, T t, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- constexpr auto conv = spec.conversion;
- if constexpr (conv == 'c')
+ const auto conv = spec.conversion;
+
+ // TODO: what is the right thing to do for %c?
+ // static_assert(std::same_as<T, int>, "%c only accepts int as argument");
+
+ if constexpr (std::signed_integral<T>)
{
- // TODO: what is the right thing to do here?
- // static_assert(std::same_as<T, int>, "%c only accepts int as argument");
+ simpleConversionSpecifierCheck("cidv", conv, "signed integral type");
}
- else if constexpr (conv == 'i' || conv == 'd')
+ else
{
- static_assert(std::signed_integral<T>, "%i and %d only accept signed integral types");
+ static_assert(std::unsigned_integral<T>, "Integral type is neither signed nor unsigned?");
+ simpleConversionSpecifierCheck("coxXuv", conv, "unsigned integral type");
}
- else if constexpr (conv == 'o' || conv == 'x' || conv == 'X' || conv == 'u')
+
+ if (spec.alternative && !"oxX"_contains(conv))
{
- static_assert(std::unsigned_integral<T>, "%o, %x, %X and %u only accept unsigned integral types");
+ throw std::invalid_argument{"‘#’ (alternative) flag is only allowed for %x, %X and %o"};
}
- else if constexpr (conv != 'v')
+
+ if (conv == 'c')
{
- static_assert(sizeof(T) == 0, "Unsupported conversion for integral type specified");
+ noPrecision(spec.precision, "integral types with %c");
}
- 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);
+ if (spec.addSign != '\0' && !(std::signed_integral<T> && "idv"_contains(conv)))
+ {
+ throw std::invalid_argument{"‘+’ and ‘ ’ (sign related) flags are only allowed for signed integral types and conversions %i, %d and %v"};
+ }
}
};
// 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<std::floating_point T>
struct formatter<T> {
- static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static void format(const format_output auto& out, T t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
{
std::chars_format format;
- bool uppercase = false;
+ const auto uppercase = "AEFG"_contains(spec.conversion);
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{"’"}};
+ assert(!"Unsupported conversion; should have been caught by formatter<T>::conversionSupported");
}
+ char* data, *end;
+ std::errc ec;
+ const auto call_to_chars = [format, precision, t, &data, &end, &ec](char* data_, std::size_t size)
+ {
+ data = data_;
+ const auto [end_, ec_] = precision ? std::to_chars(data, data + size, t, format, *precision) : std::to_chars(data, data + size, t, format);
+ end = end_;
+ ec = ec_;
+ };
+
std::array<char, (std::numeric_limits<T>::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};
+ std::unique_ptr<char[]> largeResult;
+ call_to_chars(result.data(), result.size());
+ if (ec == std::errc::value_too_large && precision)
+ {
+ const auto size = result.size() + *precision;
+ largeResult = std::make_unique_for_overwrite<char[]>(size);
+ call_to_chars(largeResult.get(), size);
+ }
+
+ if (ec != std::errc{})
+ {
+ throw std::runtime_error{"Unexpected error from to_chars: " + std::make_error_condition(ec).message()};
+ }
+
+ std::string_view converted{data, end};
const bool hasMinusSign = converted.starts_with('-');
if (hasMinusSign)
@@ -337,11 +380,9 @@ namespace format {
return;
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, T t, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- 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);
+ simpleConversionSpecifierCheck("fFeEaAgGv", spec.conversion, "floating point type");
}
};
@@ -365,11 +406,11 @@ namespace format {
formatPadded(out, val, ' ', spec.leftJustified, minWidth);
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, const S& s, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- 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);
+ simpleConversionSpecifierCheck("sv", spec.conversion, "stringy type");
+ noAlternative(spec.alternative, "stringy types");
+ noSignFlags(spec.addSign, "stringy types");
}
};
@@ -378,7 +419,10 @@ namespace format {
template<pointer T>
struct formatter<T> {
- static constexpr void format(const format_output auto& out, const std::remove_pointer_t<T>* t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr inline format_specifier delegateSpec{.conversion = 'x', .alternative = true};
+ using delegated_formatter = formatter<std::uintptr_t>;
+
+ static constexpr void format(const format_output auto& out, const std::remove_pointer_t<T>* t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t>)
{
if (t == nullptr)
{
@@ -386,31 +430,30 @@ namespace format {
}
else
{
- constexpr format_specifier spec{.conversion = 'x', .alternative = true};
- formatter<std::uintptr_t>::template format<spec>(out, std::bit_cast<std::uintptr_t>(t), minWidth, std::nullopt);
+ delegated_formatter::format(out, std::bit_cast<std::uintptr_t>(t), delegateSpec, minWidth, std::nullopt);
}
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, const std::remove_pointer_t<T>* t, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- 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);
+ simpleConversionSpecifierCheck("pv", spec.conversion, "pointer type");
+ noAlternative(spec.alternative, "pointer types");
+ noSignFlags(spec.addSign, "pointer types");
+ noPrecision(spec.precision, "pointer types");
+ delegated_formatter::conversionSupported(delegateSpec);
}
};
template<>
struct formatter<std::nullptr_t> {
- static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t>)
{
formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth);
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, std::nullptr_t, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- 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);
+ return formatter<void*>::conversionSupported(spec);
}
};
@@ -437,11 +480,11 @@ namespace format {
}
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, const char* s, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- 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);
+ simpleConversionSpecifierCheck("spv", spec.conversion, "char pointer");
+ noAlternative(spec.alternative, "char pointer");
+ noSignFlags(spec.addSign, "char pointer");
}
};
@@ -472,16 +515,14 @@ namespace format {
formatPadded(out, stream.view(), ' ', spec.leftJustified, minWidth);
}
- template<format_specifier spec>
- static constexpr void format(const format_output auto& out, const T& t, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision)
+ static constexpr void conversionSupported(format_specifier spec)
{
- 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 any 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);
+ constexpr std::string_view fallbackName{"operator<<(std::ostream&) fallback"};
+ simpleConversionSpecifierCheck("sv", spec.conversion, fallbackName);
+ noAlternative(spec.alternative, fallbackName);
+ noSignFlags(spec.addSign, fallbackName);
+ noPrecision(spec.precision, fallbackName);
+ noZeroPadding(spec.padding, fallbackName);
}
};
}
diff --git a/include/cxxformat/runtime.hpp b/include/cxxformat/runtime.hpp
new file mode 100644
index 0000000..cc09b7e
--- /dev/null
+++ b/include/cxxformat/runtime.hpp
@@ -0,0 +1,219 @@
+#pragma once
+
+#include <array>
+#include <variant>
+#include <vector>
+
+#include <cxxformat/core.hpp>
+#include <cxxformat/formatters.hpp>
+#include <cxxformat/string.hpp>
+
+namespace format {
+ namespace {
+ namespace run_time {
+ class format_template {
+ std::vector<std::variant<std::string, format_specifier>> parts;
+ std::size_t argCount;
+
+ public:
+ format_template(std::vector<std::variant<std::string, format_specifier>>&& parts, std::size_t argCount) noexcept : parts{std::move(parts)}, argCount{argCount} {}
+
+ template<format_output_compatible Output, typename... Args> requires (has_some_formatter<std::decay_t<Args>> && ...)
+ void operator()(Output&& output, Args&&... args) const
+ {
+ checkArgCount(sizeof...(Args));
+
+ std::array<optional_int<std::size_t>, sizeof...(Args)> indexArgs{[]<typename Arg>(Arg&& arg) constexpr -> optional_int<std::size_t> {
+ if constexpr (std::unsigned_integral<Arg> && sizeof(Arg) <= sizeof(std::size_t))
+ {
+ return static_cast<std::size_t>(arg);
+ }
+ else
+ {
+ return std::nullopt;
+ }
+ }(std::forward<Args>(args))...};
+
+ const auto checkPart = [&args..., &indexArgs]<std::size_t... indices>(std::index_sequence<indices...>)
+ {
+ return [&args..., &indexArgs](format_specifier spec)
+ {
+ ([spec, &indexArgs]<typename Arg>(Arg&&, std::size_t index)
+ {
+ if (index == spec.argIndex)
+ {
+ if (spec.widthAsArg)
+ {
+ if (!indexArgs[*spec.minWidth])
+ {
+ throw std::invalid_argument{"Width argument must be an unsigned integral at most as big as std::size_t"};
+ }
+ }
+ if (spec.precisionAsArg)
+ {
+ if (!indexArgs[*spec.precision])
+ {
+ throw std::invalid_argument{"Precision argument must be an unsigned integral at most as big as std::size_t"};
+ }
+ }
+ formatter_with_fallback<std::decay_t<Arg>>::conversionSupported(spec);
+ }
+ }(std::forward<Args>(args), indices), ...);
+ };
+ }(std::make_index_sequence<sizeof...(Args)>());
+
+ for (const auto& part : parts)
+ {
+ if (std::holds_alternative<format_specifier>(part))
+ {
+ checkPart(std::get<format_specifier>(part));
+ }
+ }
+
+ const auto out = make_output(std::forward<Output>(output));
+ const overloaded_callable visitor{
+ [&args..., &out, &indexArgs]<std::size_t... indices>(std::index_sequence<indices...>)
+ {
+ return [&args..., &out, &indexArgs](format_specifier spec)
+ {
+ ([&out, spec, &indexArgs]<typename Arg>(Arg&& arg, std::size_t index)
+ {
+ if (index == spec.argIndex)
+ {
+ optional_int<std::size_t> minWidth = spec.minWidth;
+ if (spec.widthAsArg)
+ {
+ minWidth = indexArgs[*minWidth];
+ assert(minWidth);
+ }
+ optional_int<std::size_t> precision = spec.precision;
+ if (spec.precisionAsArg)
+ {
+ precision = indexArgs[*precision];
+ assert(precision);
+ }
+ return formatter_with_fallback<std::decay_t<Arg>>::format(out, std::forward<Arg>(arg), spec, minWidth, precision);
+ }
+ }(std::forward<Args>(args), indices), ...);
+ };
+ }(std::make_index_sequence<sizeof...(Args)>()),
+ [&out](std::string text) { out(text); }
+ };
+ for (const auto& part : parts)
+ {
+ std::visit(visitor, part);
+ }
+ }
+
+ private:
+ void checkArgCount(std::size_t args) const
+ {
+ if (args > argCount)
+ {
+ throw std::invalid_argument{"Too many arguments passed!"};
+ }
+ else if (args < argCount)
+ {
+ throw std::invalid_argument{"Not enough arguments passed!"};
+ }
+ }
+ };
+
+ format_template parseFormat(std::string_view fmt)
+ {
+ ArgIndex nextArg{0};
+ ArgIndex maxArg{0};
+
+ std::vector<std::variant<std::string, format_specifier>> parts;
+
+ const auto addString = [&parts](std::string_view part)
+ {
+ if (!parts.empty() && std::holds_alternative<std::string>(parts.back()))
+ {
+ std::get<std::string>(parts.back()).append(part);
+ }
+ else
+ {
+ parts.emplace_back(std::string{part});
+ }
+ };
+
+ for (std::size_t pos{0}; pos < fmt.size(); )
+ {
+ const auto nextSpec = fmt.find('%', pos);
+ if (nextSpec == std::string_view::npos)
+ {
+ parts.emplace_back(std::string{fmt.substr(pos)});
+ break;
+ }
+
+ if (nextSpec + 1 >= fmt.size())
+ {
+ throw std::invalid_argument{"Incomplete format specifier at end of format string"};
+ }
+
+ const auto specStart = fmt[nextSpec + 1];
+ if (specStart == '%')
+ {
+ // + 1 to include the first %
+ addString(fmt.substr(pos, nextSpec - pos + 1));
+ pos = nextSpec + 2;
+ }
+ else
+ {
+ const auto [newPos, spec, newNextArg, specMaxArg] = parseSpec(fmt, nextSpec + 1, nextArg);
+ if (!checkValidConversion(spec.conversion))
+ {
+ throw std::invalid_argument{"Invalid conversion specifier ‘%c’ in format specifier “%s”"_format(spec.conversion, fmt.substr(nextSpec, newPos - nextSpec))};
+ }
+
+ addString(fmt.substr(pos, nextSpec - pos));
+
+ nextArg = newNextArg;
+ maxArg = std::max(maxArg, specMaxArg);
+ pos = newPos;
+
+ parts.emplace_back(spec);
+ }
+ }
+
+ return {std::move(parts), maxArg};
+ }
+ }
+ }
+
+ template<format_output_compatible Output, typename... Args> requires (has_some_formatter<std::decay_t<Args>> && ...)
+ void format_to(std::string_view fmt, Output&& output, Args&&... args)
+ {
+ run_time::parseFormat(fmt)(std::forward<Output>(output), std::forward<Args>(args)...);
+ }
+
+ template<format_output_compatible Output, typename... Args> requires (has_some_formatter<std::decay_t<Args>> && ...)
+ void format_nothrow_to(std::string_view fmt, Output&& output, Args&&... args) noexcept
+ {
+ try
+ {
+ format_to(fmt, std::forward<Output>(output), std::forward<Args>(args)...);
+ }
+ catch(const std::exception& e)
+ {
+ "Formatting error: %s"_format_to(output, e.what());
+ }
+ }
+
+ template<typename... Args> requires (has_some_formatter<std::decay_t<Args>> && ...)
+ std::string format(std::string_view fmt, Args&&... args)
+ {
+ std::ostringstream out;
+ format_to(fmt, out, std::forward<Args>(args)...);
+ return out.str();
+ }
+
+ template<typename... Args> requires (has_some_formatter<std::decay_t<Args>> && ...)
+ std::string format_nothrow(std::string_view fmt, Args&&... args)
+ {
+ std::ostringstream out;
+ format_nothrow_to(fmt, out, std::forward<Args>(args)...);
+ return out.str();
+ }
+}
diff --git a/main.cpp b/main.cpp
index f448ed7..6c5ba3d 100644
--- a/main.cpp
+++ b/main.cpp
@@ -18,6 +18,19 @@ void numberTests()
int main(int argc, char* argv[])
{
using namespace format;
+ using format::format;
+
+ if (argc == 2)
+ {
+ std::string_view fmt{argv[1]};
+ format_nothrow_to(fmt, stdout, "Knirp", 's', argv[0], 42u, 1.337);
+ putchar('\n');
+ return 0;
+ }
+
+ format_to("Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n", std::cout, "Knirp", 's', argv[0], 42u, 1.337);
+ assert(format("Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n", "Knirp", 's', argv[0], 42u, 1.337) == "Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n"_format("Knirp", 's', argv[0], 42u, 1.337));
+
"-%20g-\n"_format_to(stdout, 0x1.8p0);
"-%20.0a-\n"_format_to(stdout, 0.0);
"-%#+20.0a-\n"_format_to(stdout, 1.0);