c++templatesbuildformatc++20

Why this template won't compile?


I have written a class called small_vector<typename T, size_t N> where T is the value_type and N is the bucket size. I had a view class defined in it:

template <typename T, size_t N> 
class small_vector {
   ....
   class view {
      ...
   };
};

Not I want to customize a formatter for it, I wrote like this:

template<typename T, size_t N>
using small_vector_view = typename small_vector<T, N>::view;

template<typename T, size_t N> struct std::formatter<typename small_vector<T, N>::view, char> {
  enum class Style { Default, Compact, Pretty };
  Style style = Style::Default;

  constexpr auto parse(format_parse_context &ctx)
  {
    auto it = ctx.begin();
    if (it == ctx.end() || *it == '}')
      return it;

    switch (*it) {
      case 'c':
        style = Style::Compact;
        break;
      case 'p':
        style = Style::Pretty;
        break;
      default:
        throw format_error("invalid format specifier for small_vector");
    }
    return ++it;
  }

  auto format(const typename small_vector<T, N>::view &v, format_context &ctx) const
  {
    if (v.empty()) {
      if (style == Style::Pretty) {
        return format_to(ctx.out(), "[\n]");
      }
      return format_to(ctx.out(), "[]");
    }

    std::string result;
    if (style == Style::Pretty) {
      result = "[\n  ";
    }
    else {
      result = "[";
    }

    for (size_t i = 0; i < v.size(); ++i) {
      if (i > 0) {
        if (style == Style::Pretty) {
          result += ",\n  ";
        }
        else if (style == Style::Compact) {
          result += ",";
        }
        else {
          result += ", ";
        }
      }
      if constexpr (std::is_same_v<T, std::string>) {
        result += std::format("\"{}\"", v[i]);
      }
      else {
        result += std::format("{}", v[i]);
      }
    }

    if (style == Style::Pretty) {
      result += "\n]";
    }
    else {
      result += "]";
    }

    return format_to(ctx.out(), "{}", result);
  }
};

But it won't compile, and the compiler complains:

D:\repos\ustring\small_vector.h(602): error C2764: 'T': template parameter not used or deducible in partial specialization 'std::formatter<small_vector<T,InlineCapacity>::view,char>'
D:\repos\ustring\small_vector.h(602): error C2764: 'N': template parameter not used or deducible in partial specialization 
'std::formatter<small_vector<T,InlineCapacity>::view,char>'
D:\repos\ustring\small_vector_tests.cpp(557): error C7595: 'std::basic_format_string<char,small_vector<int,16>::view &>::basic_format_string': call to immediate function is not a constant expression

I'm using the newest MSVC on x64 Windows, I wonder why it cannot deduce T and N, how can I make this piece of code compile?

(full code: https://github.com/nekomiya-kasane/temp-utils/blob/main/small_vector.h, last 2 functions)


Solution

  • T and N are used in an non-deduced context, and can therefore not be deduced.

    See Non-deduced contexts (first bullet point):

    If a template parameter is used only in non-deduced contexts and is not explicitly specified, template argument deduction fails.

    1. The nested-name-specifier (everything to the left of the scope resolution operator ::) of a type that was specified using a qualified-id

    in your case:

    //                    /--------- qualified-id --------\
    //                    |                               |
    struct std::formatter<typename small_vector<T, N>::view, char>
    //                             |                  |
    //                             \ nested-name-spec./
    //                                 (non-deduced)
    

    See Partial specialization of a class for a nested class of a template class for a potential workaround.


    In your case you could work around this by adding an alias to your small_vector::view that resolves to the small_vector that contains it, e.g.:

    template<class T, std::size_t N>
    class small_vector {
      // ...
    public:
      class view {
        // ...
      public:
        using small_vector_type = small_vector;
      };
    };
    

    ... then you can write a concept that checks if a given type is a small_vector::view by checking the small_vector_type alias:

    template<class T>
    constexpr inline bool is_small_vector_v = false;
    
    template<class T, std::size_t N>
    constexpr inline bool is_small_vector_v<small_vector<T, N>> = true;
    
    template<class T>
    concept small_vector_view_type = requires() {
        // has a small_vector_type alias
        typename T::small_vector_type;
        // small_vector_type is a small_vector
        requires is_small_vector_v<typename T::small_vector_type>;
        // small_vector_type::view is equivalent to T
        requires std::same_as<typename T::small_vector_type::view, T>;
    };
    

    ...and then this concept can be used to write a partial specialization for small_vector::view:

    template<small_vector_view_type T>
    struct std::formatter<T, char> {
      template<class ParseContext>
      constexpr ParseContext::iterator parse(ParseContext& ctx) { /* ... */ }
    
      template<class FmtContext>
      FmtContext::iterator format(T const& view, FmtContext& ctx) const { /* ... */ }
    };