Search code examples
c++c++20metaprogrammingfmt

How does template argument deduction work in this fmt lib's format_string?


I encountered some difficulties trying to understand the fmt lib.

Let's start from the print function:

fmt::print("Hello, {}", a);

This is the defination of print:

template <typename... T>
FMT_INLINE void print(format_string<T...> fmt, T&&... args) {
    // Implementation
}

The print function takes a format_string<T...> as argument. So we can do something with T... at compile time.

But how can be T... be deduced at compile-time?

The following is how format_string is implemented (code taken from version 10.0.0). For compile-time format string, it has a implicit constexpr constructor that takes a string_view-compatible type as argument.

template <typename... Args>
using format_string = basic_format_string<char, type_identity_t<Args>...>;

template <typename Char, typename... Args> class basic_format_string {
 private:
  basic_string_view<Char> str_;

 public:
  template <typename S,
            FMT_ENABLE_IF(
                std::is_convertible<const S&, basic_string_view<Char>>::value)>
  FMT_CONSTEVAL FMT_INLINE basic_format_string(const S& s) : str_(s) {
    static_assert(
        detail::count<
            (std::is_base_of<detail::view, remove_reference_t<Args>>::value &&
             std::is_reference<Args>::value)...>() == 0,
        "passing views as lvalues is disallowed");
#ifdef FMT_HAS_CONSTEVAL
    if constexpr (detail::count_named_args<Args...>() ==
                  detail::count_statically_named_args<Args...>()) {
      using checker =
          detail::format_string_checker<Char, remove_cvref_t<Args>...>;
      detail::parse_format_string<true>(str_, checker(s));
    }
#else
    detail::check_format_string<Args...>(s);
#endif
  }
  basic_format_string(runtime_format_string<Char> fmt) : str_(fmt.str) {}

  FMT_INLINE operator basic_string_view<Char>() const { return str_; }
  FMT_INLINE auto get() const -> basic_string_view<Char> { return str_; }
};

However, I failed to find a user-defined deduction guide or anything like that in the source code. So how is type arguments Args... deduced for template class basic_format_string?


Solution

  • Expanding the alias format_string gives you

    template <typename... T>
    FMT_INLINE void print(basic_format_string<char, type_identity_t<T>...> fmt, T&&... args) {
        // Implementation
    }
    

    type_identity_t<T> is also an alias for type_identity<T>::type:

    template <typename... T>
    FMT_INLINE void print(basic_format_string<char, type_identity<T>::type...> fmt, T&&... args) {
        // Implementation
    }
    

    Now in the first function parameter the template parameter T appears only on the left-hand side of a nested name specifier, which makes it a non-deduced context, meaning that the function parameter will not be used to deduce T.

    So, the first function parameter does not participate in template argument deduction. Instead T is deduced only from the other function parameters, i.e. T... are just the template arguments deduced from the T&&... args forwarding reference pack.