I am familiar with how to specialize std::formatter; it's relatively easy and clear with explicit specialization.
But with partial specialization - its a challenge - due to shifting nature of various libraries (including the c++ standard library) and what partial specializations are already included (and sometimes even implementation details of how they are implemented - just what they match).
I have a library, with a bunch of provided types, and which attempts to work with a variety of std c++ versions (20, 23, 26) and have those types work with formatter<>.
Background (From Stroika 'ToString.h')
// Does type 'T' support the Stroika ToString() API
template <typename T>
concept IToString = requires (T t) { { ToString (t) } -> convertible_to<Characters::String>; };
...
template <IToString T>
struct ToStringFormatter /* : std::formatter<std::wstring>*/{...}
Then I want to say something like - if the standard formatter is not already defined, then use ToStringFormatter (never mind about the namespaces for simplicity):
template <typename T>
requires (!std::formattable<T,wchar_t> && IToString<T>)
struct std::formatter<T, wchar_t> : ToStringFormatter<T> {};
So if the type isn't already formattable, format it with ToStringFormatter<>. This appears like it should be the most simple, and natural approach, but doesn't work on visual studio ( error C7608: atomic constraint should be a constant expression) presumably because the definition actually changes the value returned by the constraint.
OR
template <IToString T>
struct std::formatter<T, wchar_t> : ToStringFormatter<T> {};
The problem with the second approach is that it introduces ambiguity, if used with a type T which is BOTH IToString and formattable.
So - I've settled on an ad-hoc approach:
template <typename T>
concept IUseToStringFormatterForFormatter_ =
// most user-defined types captured by this rule - just add a ToString() method!
requires (T t) {
{
t.ToString ()
} -> convertible_to<Characters::String>;
}
// Stroika types not captured by std-c++ rules
or Common::IKeyValuePair<remove_cvref_t<T>> or Common::ICountedValue<remove_cvref_t<T>>
// c++ 23 features which may not be present with current compilers
// value with clang++16 was 202101L and cpp2b and libc++ (ubuntu 23.10 and 24.04) flag... and it had at least the pair<> code supported.
// this stuff needed for clang++-18-debug-libstdc++-c++23
#if !__cpp_lib_format_ranges
#if !qHasFeature_fmtlib or (FMT_VERSION < 110000)
or (ranges::range<decay_t<T>> and
not Configuration::IAnyOf<decay_t<T>, string, wstring, string_view, wstring_view, const char[], const wchar_t[],
qStroika_Foundation_Characters_FMT_PREFIX_::string_view, qStroika_Foundation_Characters_FMT_PREFIX_::wstring_view>)
#endif
#endif
// sadly MSFT doesn't support all, and doesn't support __cplusplus with right value
// 202302L is right value to check for C++ 23, but 202101L needed for clang++16 ;-(
#if _MSC_VER || __cplusplus < 202101L /*202302L 202100L 202300L*/ || (__clang__ != 0 && __GLIBCXX__ != 0 && __GLIBCXX__ <= 20240412) || \
(!defined(__clang__) && __cplusplus == 202302L && __GLIBCXX__ <= 20240412) and (!defined(_LIBCPP_STD_VER) || _LIBCPP_STD_VER < 23)
#if !qHasFeature_fmtlib or (FMT_VERSION < 110000)
// available in C++23
or Configuration::IPair<remove_cvref_t<T>> or
Configuration::ITuple<remove_cvref_t<T>>
#endif
#endif
// need to check _LIBCPP_STD_VER for LIBC++ and clang++16 on ubuntu 23.10
#if (!defined(__cpp_lib_formatters) || __cpp_lib_formatters < 202302L) and (!defined(_LIBCPP_STD_VER) || _LIBCPP_STD_VER < 23)
// available in C++23
or Configuration::IAnyOf<remove_cvref_t<T>, thread::id>
#endif
// features added in C++26
// unsure what to check - __cpp_lib_format - test c++26 __cpp_lib_formatters < 202601L -- 202302L is c++23
#if __cplusplus < 202400L
or Configuration::IAnyOf<remove_cvref_t<T>, std::filesystem::path>
#endif
// Features from std-c++ that probably should have been added
// NOTE - we WANT to support type_info (so typeid works directly) - but run into trouble because type_info(const type_info)=delete, so not
// sure how to make that work with formatters (besides wrapping in type_index).
or is_enum_v<remove_cvref_t<T>> or Configuration::IOptional<remove_cvref_t<T>> or Configuration::IVariant<remove_cvref_t<T>>
#if qCompilerAndStdLib_ITimepointConfusesFormatWithFloats_Buggy
or same_as<T, std::chrono::time_point<chrono::steady_clock, chrono::duration<double>>>
#else
or Configuration::ITimePoint<T>
#endif
or Configuration::IAnyOf<remove_cvref_t<T>, exception_ptr, type_index> or derived_from<T, exception>;
};
template <Private_::IUseToStringFormatterForFormatter_ T>
struct std::formatter<T, wchar_t> : ToStringFormatter<T> {};
This sort of approach appears to perhaps work - but with lots of exceptions and problems (principally that different libraries - like fmtlib - and clang libc++ etc) all seem to implement the 'ranges' support differently, so the fact that ranges are integrated automatically into std::formater<> - is very complicated to check for each given type and combination of libraries, which checks to throw into IUseToStringFormatterForFormatter_.
Even the complexity above is not nearly enough (and yet seems too much to be very convenient).
Really, what I think would be ideal, is (what I tried first) - something along the lines of:
template <typename T>
requires (!std::formattable_SO_FAR<T,wchar_t>)
struct std::formatter<T, wchar_t> : ToStringFormatter<T> {};
where formattable_SO_FAR is allowed to change meaning as the compilation progresses.
Is there some way with the current C++ language to conditionally extend std::formatter using checks whose value depends on 'formattable' - so that the return value of formattable would change in different parts of the program?
std::formatter
, the immediate problem you mention is solved, but another problem replaces it: Even if the concept itself is atomic, it will ODR-use the instantiation ofstd::formatter<T>
when testing, probably using the primary template definition which doesn't even satisfystd::semiregular<std::formatter<T>>
, but that ODR-usage prevents the specialization from being accessible because it's already defined as the primary template at that point. – Wilfordwilfredtypename TUnique = decltype([]() -> void {})
default parameter and namedtypename TUnique;
within therequires
expression, so that each instantiation is a unique atomic constraint which can yield a different result in different parts of the program. But like I said, unfortunately that's not enough on its own to achieve what you want here :\ If I think of a way around that problem too, then I'll edit and undelete my answer, but I don't currently have a solution for you. – Wilfordwilfredmy_formatter
and use a function likemy_format
that first try to format variables viamy_formatter
and then delegate tostd::formatter
. This way, your library and formatting rules are encapsulated from other libraries, so there won't be interferences. – Fascesmy_lib_wrap<T>
template class and put all your formatter specializations under it, i.e., you only create specializations likestd::formatter<my_lib_wrap<T>>
and when user wants to print, say a rangeR
, using your rules they writestd::format("{}", my_lib_wrap(R))
. – Fasces