Templating a template specialization

63 Views Asked by At

I have the following template to use fmt to format a user-defined type:

#include "fmt/format.h"

struct MyClass
{
    operator std::string() const
    {
        return "xyz";
    }
};

template <> struct fmt::formatter<MyClass>
{
    template <typename ParseContext> constexpr auto parse(ParseContext& ctx) const {
        return ctx.begin();
    }
    template <typename FormatContext> constexpr auto format(const MyClass& c, FormatContext& ctx) const {
        return format_to(ctx.out(), "{}", (std::string)c);
    }
};

I attempted to template and instantiate this template specialization:

template <typename T> template <> struct fmt::formatter<T>
{
    template <typename ParseContext> constexpr auto parse(ParseContext& ctx) const {
        return ctx.begin();
    }
    template <typename FormatContext> constexpr auto format(const T& t, FormatContext& ctx) const {
        return format_to(ctx.out(), "{}", (std::string)t);
    }
};

template struct fmt::formatter<MyClass>;

I get error C2913: explicit specialization; 'fmt::v7::formatter<MyClass,char,void>' is not a specialization of a class template.

How can I accomplish this, or some other way to make it simpler to turn a type with overloaded operator std::string() into a type that can be formatted?

1

There are 1 best solutions below

3
Miles Budnek On

If you want to have re-usable formatter that you can explicitly define for any class, the easiest thing to do would be to define a base class and derive fmt::formatter<T> from it:

template <typename T>
struct StringCastFormatter
{
    template <typename ParseContext> constexpr auto parse(ParseContext& ctx) const {
        return ctx.begin();
    }
    template <typename FormatContext> constexpr auto format(const T& c, FormatContext& ctx) const {
        return format_to(ctx.out(), "{}", (std::string)c);
    }
};

template <>
struct fmt::formatter<MyClass> : StringCastFormatter<MyClass> {};

Demo


If instead you want to define a formatter that automatically applies to any class that can be converted to std::string, then you need to use either C++20 concepts or some form of SFINAE.

Possible C++20 concepts implementation:

template <std::convertible_to<std::string> T>
struct fmt::formatter<T>
{
    template <typename ParseContext> constexpr auto parse(ParseContext& ctx) const {
        return ctx.begin();
    }
    template <typename FormatContext> constexpr auto format(const T& c, FormatContext& ctx) const {
        return format_to(ctx.out(), "{}", (std::string)c);
    }
};

Demo

And a possible C++17 SFINAE-based solution:

template <typename T, typename...>
using First = T;

template <typename T>
struct fmt::formatter<First<T, std::enable_if_t<std::is_convertible_v<T, std::string>>>>
{
    template <typename ParseContext> constexpr auto parse(ParseContext& ctx) const {
        return ctx.begin();
    }
    template <typename FormatContext> constexpr auto format(const T& c, FormatContext& ctx) const {
        return format_to(ctx.out(), "{}", (std::string)c);
    }
};

Demo