4

I have a class that takes a C printf set of variadic arguments in its constructor, like this:

class Foo {
public:
    Foo(const char* str, ...) __attribute__((format(printf, 2, 3)));

Now I want to be able to use the fmt library with this class. If I were willing to change all callers I could switch it something like this:

class Foo {
public:
    template<typename Str, typename... Args>
    Foo(const Str& str, const Args&... args)
        : Foo(str, fmt::make_args_checked<Args...>(str, args...))
    {}

private:
    Foo(fmt::string_view fmt, fmt::format_args args);

But this class is used in 100's of places and it's not feasible to "change the world". So I want to keep both constructors, but obviously now I need a way to choose between them. I'm not excited about having to add a new dummy parameter or something.

Then I thought, well, I'd also really like to enforce using the FMT_STRING() macro since my printf-style code takes advantage of printf format checking in GCC and clang. So maybe I could do something with that: I could create my own macro, say MYFMT(), that would invoke FMT_STRING() somehow, or at least do the same checking, but then resolve to my own type that could be used to choose a different constructor; something like:

#define MYFMT(_f) ...
class Foo {
public:
    Foo(const char* str, ...);
    Foo(const MyType& str, ...) ...

So, the usage would be something like:

    auto x = Foo("this is a %s string", "printf");
    auto y = Foo(MYFMT("this is a {} string"), "fmt");

But I have played with this for a few hours and tried to wrap my head around how the FMT_STRING macro works and what I'd need to do, and I can't come up with anything. Maybe this isn't possible for some reason but if anyone has any hints that would be great.

FWIW my base compilers are GCC 10, clang 9, and MSVC 2019 so I can rely on C++17 at least.

MadScientist
  • 92,819
  • 9
  • 109
  • 136

1 Answers1

1

You can do it as follows (godbolt):

#include <fmt/core.h>

struct format_string {
  fmt::string_view str;

  constexpr operator fmt::string_view() const { return str; }
};

#define MYFMT(s) format_string{s}

class Foo {
 public:
  template <typename... T>
  Foo(const char* str, T&&... args) {
    fmt::print("printf\n");
  }

  template <typename... T>
  Foo(fmt::format_string<T...> str, T&&... args) {
    fmt::print("fmt\n");
  }
};

int main() {
  Foo("%d\n", 42);        // calls the printf overload
  Foo(MYFMT("{}\n"), 42); // calls the fmt overload
}

With C++20 this will give you compile-time checks in {fmt}. Note that varargs are replaced with variadic templates in the printf overload to avoid ambiguity so you won't be able to apply the format attribute. It might be possible to keep varargs by tweaking this solution a bit.

A better option would be to avoid overloading and macros altogether and use a different function instead:

class Foo {
 private:
  Foo() {}
  
 public:
  Foo(const char* str, ...) {
    fmt::print("printf\n");
  }

  template <typename... T>
  static Foo format(fmt::format_string<T...> str, T&&... args) {
    fmt::print("fmt\n");
    return Foo();
  }
};
vitaut
  • 49,672
  • 25
  • 199
  • 336
  • Thanks for the info; I'll take a look and see if I can make it work. Unfortunately I don't have access to C++20 everywhere. Also it won't be easy to get rid of variadic methods: they don't just print something out: they generate the text into string buffers and the va_list is passed around etc. I don't think it will work in my environment to have people calling static methods instead of constructors that's why I was hoping to avoid ambiguity with a different type. Maybe that can't work: I don't really understand what magic format uses to implement compile-time format checking. – MadScientist Jun 27 '21 at 04:08
  • You can still use variadics internally, only not the format attribute. – vitaut Jun 27 '21 at 13:13