c++formattingoverloading

Is there a way to disambiguate overloads wrapping std::format and std::vformat


First of all, some demo code:

#include <format>
#include <iostream>

// OVERLOAD 1
template <typename... Ts>
inline void write(const std::format_string<Ts...> &fmtStr, Ts &&...args)
{
  std::string msg{std::format(fmtStr, std::forward<Ts>(args)...)};
  std::cout << msg << '\n';
}

// OVERLOAD 2
template <typename... Ts>
inline void write(const std::string_view fmtStr, Ts &&...args)
{
  std::string msg{std::vformat(fmtStr, std::make_format_args(std::forward<Ts>(args)...))};
  std::cout << msg << '\n';
}

std::string foo()
{
  return "Another test {}";
}

int main()
{
  write("Test {}", "a value"); // CALL 1 ambiguous call to overloaded function
  write(foo(), "a value");     // CALL 2 ambiguous call to overloaded function

  write(std::format_string<int>("Test3 {}"), 3); // CALL 3
  write(std::string_view("Test4 {}"), 3);        // CALL 4
}

The vision here is to have call 1 find overload 1, since std::format_string requires fmtStr to be consteval, and likewise, call 2 should find overload 2 because the value from foo is not consteval.

Questions:

  1. Is it possible to disambiguate these calls 1 and 2 while still allowing string literals to be accepted. I know that they can be disambiguated by being explicit as in calls 3 and 4. This is C++23 so I'd be pleased to have an easy solution via concepts/constraints.
  2. Is this even a reasonable pursuit? At least with MSVC std::format_string is a basic_format_string, which is a thin wrapper around a basic_string_view, and std::format just calls std::vformat and does nothing else. This implies to me that there's no real performance implication, all I'm losing is the compile-time format string checking. I'd love to keep the compile-time checking, but recognize it's not necessary.

Solution

  • Yes you will need two more overloads (to get better matches with const char* and std::string)

    #include <type_traits>
    #include <format>
    #include <iostream>
    
    // OVERLOAD 1
    template <typename... Ts>
    inline void write(std::format_string<Ts...>&& fmtStr, Ts&&...args) 
    {
      std::string msg{std::format(fmtStr, std::forward<Ts>(args)...)};
      std::cout << msg << '\n';
    }
    
    // OVERLOAD 2
    template <typename... Ts>
    inline auto write(std::string_view fmtStr, Ts&&...args) 
    {
      std::string msg{std::vformat(fmtStr, std::make_format_args(std::forward<Ts>(args)...))};
      std::cout << msg << '\n';
    }
    
    // OVERLOAD 3
    template <typename... Ts>
    inline auto write(const char* fmtStr, Ts&&...args) 
    {
      std::string msg{std::vformat(fmtStr, std::make_format_args(std::forward<Ts>(args)...))};
      std::cout << msg << '\n';
    }
    
    // OVERLOAD 4
    template <typename... Ts>
    inline auto write(const std::string& fmtStr, Ts&&...args) 
    {
      std::string msg{std::vformat(fmtStr, std::make_format_args(std::forward<Ts>(args)...))};
      std::cout << msg << '\n';
    }
    
    std::string foo()
    {
      return "Another test {}";
    }
    
    int main()
    {
      write("Test {}", "a value"); // CALL 1 ambiguous call to overloaded function
      write(foo(), "a value");     // CALL 2 ambiguous call to overloaded function
    
      write(std::format_string<int>("Test3 {}"), 3); // CALL 3
      write(std::string_view("Test4 {}"), 3);        // CALL 4
    }