Skip to content

Trying to improve errors in the unformattable case. - #3478

Merged
vitaut merged 5 commits into
fmtlib:masterfrom
brevzin:better-unformattable-errors
Jul 1, 2023
Merged

vitaut merged 5 commits into
fmtlib:masterfrom
brevzin:better-unformattable-errors

Conversation

@brevzin

@brevzin brevzin commented Jun 4, 2023

Copy link
Copy Markdown
Contributor

This is my attempt to improve some some of the compile errors that we get when types aren't formattable at all.

Example 1:

void test(X x, Y y, Z z) {
    fmt::vprint("{} {} {}\n", fmt::make_format_args(x, y, z));
}

Before - you know something is unformattable, but you don't know which one: is it the X, the Y, or the Z?

<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false
ASM generation compiler returned: 1
<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false

After: obviously the Y:

<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1573:63: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 1573 |     type_is_unformattable_for<T, typename Context::char_type> _;
      |                                                               ^
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false
ASM generation compiler returned: 1
<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2936:52:   required from here
<source>:1573:63: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 1573 |     type_is_unformattable_for<T, typename Context::char_type> _;
      |                                                               ^
<source>:1577:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter<T> specialization: https://fmt.dev/latest/api.html#udt
 1577 |       formattable,
      |       ^~~~~~~~~~~
<source>:1577:7: note: 'formattable' evaluates to false

Example 2, which is the more typical way people run into this:

void test(X x, Y y, Z z) {
    fmt::print("{} {} {}\n", x, y, z);
}

Before (just copying the first error). Here, you can tell that Y isn't formattable, but the message is about something not being default constructible, which is a little cryptic?

<source>: In instantiation of 'constexpr decltype (ctx.begin()) fmt::v10::detail::parse_format_specs(ParseContext&) [with T = Y; ParseContext = compile_parse_context<char>; decltype (ctx.begin()) = const char*]':
<source>:2607:22:   required from 'constexpr fmt::v10::detail::format_string_checker<Char, Args>::format_string_checker(fmt::v10::basic_string_view<Char>) [with Char = char; Args = {X, Y, Z}]'
<source>:2937:15:   required from here
<source>:2549:10: error: use of deleted function 'fmt::v10::formatter<T, Char, Enable>::formatter() [with T = Y; Char = char; Enable = void]'
 2549 |   return formatter<mapped_type, char_type>().parse(ctx);
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:1066:3: note: declared here
 1066 |   formatter() = delete;
      |   ^~~~~~~~~

After: clearly Y is unformattable:

<source>: In instantiation of 'constexpr decltype (ctx.begin()) fmt::v10::detail::parse_format_specs(ParseContext&) [with T = Y; ParseContext = compile_parse_context<char>; decltype (ctx.begin()) = const char*]':
<source>:2607:22:   required from 'constexpr fmt::v10::detail::format_string_checker<Char, Args>::format_string_checker(fmt::v10::basic_string_view<Char>) [with Char = char; Args = {X, Y, Z}]'
<source>:2937:15:   required from here
<source>:2545:45: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 2545 |     type_is_unformattable_for<T, char_type> _;
      |                                             ^
<source>: In instantiation of 'constexpr fmt::v10::detail::value<Context> fmt::v10::detail::make_arg(T&) [with bool PACKED = true; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; T = Y; typename std::enable_if<PACKED, int>::type <anonymous> = 0]':
<source>:1805:51:   required from 'constexpr fmt::v10::format_arg_store<Context, Args>::format_arg_store(T& ...) [with T = {X, Y, Z}; Context = fmt::v10::basic_format_context<fmt::v10::appender, char>; Args = {X, Y, Z}]'
<source>:1823:18:   required from 'constexpr fmt::v10::format_arg_store<Context, typename std::remove_cv<typename std::remove_reference<T>::type>::type ...> fmt::v10::make_format_args(T& ...) [with Context = basic_format_context<appender, char>; T = {X, Y, Z}]'
<source>:2870:44:   required from 'void fmt::v10::print(format_string<T ...>, T&& ...) [with T = {X&, Y&, Z&}; format_string<T ...> = basic_format_string<char, X&, Y&, Z&>]'
<source>:2937:15:   required from here
<source>:1573:63: error: 'fmt::v10::detail::type_is_unformattable_for<Y, char> _' has incomplete type
 1573 |     type_is_unformattable_for<T, typename Context::char_type> _;
      |                                                               ^

Maybe it's possible to do better than this, I'm not sure, but this is the best I've been able to come up with so far at least.

@vitaut

vitaut commented Jun 4, 2023

Copy link
Copy Markdown
Contributor

Having the type name displayed in diagnostic would be nice but unfortunately you can't use if constexpr unconditionally.

@vitaut

vitaut commented Jun 16, 2023

Copy link
Copy Markdown
Contributor

@brevzin, do you plan to update this PR?

@brevzin

brevzin commented Jun 18, 2023

Copy link
Copy Markdown
Contributor Author

@brevzin, do you plan to update this PR?

Yeah I will. Are you okay with a solution that just gives the better error in C++17, and maintains status quo before then?

@vitaut

vitaut commented Jun 18, 2023

Copy link
Copy Markdown
Contributor

Are you okay with a solution that just gives the better error in C++17, and maintains status quo before then?

I am fine with that.

@brevzin

brevzin commented Jun 19, 2023

Copy link
Copy Markdown
Contributor Author

Are you okay with a solution that just gives the better error in C++17, and maintains status quo before then?

I am fine with that.

Done.

Comment thread include/fmt/core.h Outdated
Comment thread include/fmt/core.h Outdated
Comment on lines +1553 to +1557
#if defined(__cpp_if_constexpr)
if constexpr (!formattable_char) {
type_is_unformattable_for<T, typename Context::char_type> _;
}
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the message below better explains the error, so let's remove this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still get both errors - the static_assert message is still printed. This way you also get which character type (and pointer type, for the next one) you were trying to print, which I think is still helpful info.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is this information actually useful? You cannot make the types in question formattable and I think having two errors may be confusing.

Comment thread include/fmt/core.h Outdated
Comment on lines +1565 to +1569
#if defined(__cpp_if_constexpr)
if constexpr (!formattable_pointer) {
type_is_unformattable_for<T, typename Context::char_type> _;
}
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Comment thread include/fmt/core.h
Comment on lines +2543 to +2551
#if defined(__cpp_if_constexpr)
if constexpr (std::is_default_constructible_v<
formatter<mapped_type, char_type>>) {
return formatter<mapped_type, char_type>().parse(ctx);
} else {
type_is_unformattable_for<T, char_type> _;
return ctx.begin();
}
#else

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to duplicate the logic here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I could do something like this instead:

if constexpr (/* not default constructible */) {
    type_is_unformattable_for<T, char_type> _;
}
return formatter<mapped_type, char_type>().parse(Ctx);

But doing it this way gives you both the good slightly better error message with type_is_unformattable and also the "not default constructible" error message when you actually try to construct the thing. But all the latter one tells you is that... something failed, whereas the former tells you what failed. I think it's better to just provide the one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why we care about checking default constructibility at all. It should be uncommon and not worth any extra diagnostics.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why we care about checking default constructibility at all. It should be uncommon and not worth any extra diagnostics.

Because that's what actually fails here. Today, if MyType is not formattable and you try to format a MyType, the error that you get is:

<source>:2549:10: error: use of deleted function 'fmt::v10::formatter<T, Char, Enable>::formatter() [with T = MyType; Char = char; Enable = void]'
 2549 |   return formatter<mapped_type, char_type>().parse(ctx);
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

That's the only way of checking if something is formattable, as far as I'm aware. has_formatter does the same thing.

@vitaut
vitaut merged commit de4705f into fmtlib:master Jul 1, 2023
@vitaut

vitaut commented Jul 1, 2023

Copy link
Copy Markdown
Contributor

Merged, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants