We can customize the (printing) format of a given class by using the specialization of formatter.

#include <format>
#include <iostream>

struct Frac {
   int a, b;
};

template <>
struct std::formatter<Frac> : std::formatter<string_view> {
	// parse() is inherited from the base class std::formatter<string_view>

    // * an efficient solution:
     auto format(const Frac& frac, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "{}/{}", frac.a, frac.b);
    }
    // the same functionality as above, but inefficient due to the temporary string
    // auto format(const Frac& frac, std::format_context& ctx) const {
    //     std::string temp;
    //     std::format_to(std::back_inserter(temp), "{}/{}", 
    //                    frac.a, frac.b);
    //     return std::formatter<string_view>::format(temp, ctx);
    // }
};
void print(std::string_view fmt,auto&&...args){
    std::cout << std::vformat(fmt, std::make_format_args(std::forward<decltype(args)>(args)...));
}

int main()
{

Frac f{ 1,10 };
print("{}", f); //  prints "1/10"

}
  1. ctx: provides access to formatting state consisting of the formatting arguments and the output iterator.
  2. ctx.out(): the iterator to the output buffer
  3. std::format_to(): append parts of the formatted output to the target destination.