-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fmtlib relies on arg_mapper
implicit ctor declaration before class specialization is made complete.
#2635
Comments
That's an interesting one, thanks for reporting.
I don't think so.
Yes, done in ac1b5f3. Does it solve the issue? |
Yes, that does fix it. It's a bummer there is no diagnostic to indicate when an incomplete type is used in this way. |
Pointed all links to the correct commit instead of master for future reference. |
@seanbaxter, although the issue has been fixed, I'm still trying to make sure that I fully understand the problem and have a few questions.
Line 1345 contains the Line 1345 in 9d5b9de
Why does it try to construct Line 1342 in 9d5b9de
is_constructible ?
Are you suggesting that this might be the same issue? I tried disabling the workaround after the current fix but it didn't help: https://github.com/fmtlib/fmt/runs/4479740148?check_suite_focus=true. |
My bad. Yes, the member function The template <typename T,
FMT_ENABLE_IF(
std::is_constructible<basic_string_view<char_type>, T>::value &&
!is_string<T>::value && !has_formatter<T, Context>::value &&
!has_fallback_formatter<T, char_type>::value)>
FMT_CONSTEXPR FMT_INLINE auto map(const T& val)
-> basic_string_view<char_type> {
return basic_string_view<char_type>(val);
}
I redid my investigation, and think this is a cleaner presentation: Starting from the top: 1instantiate function Line 3098 in fd62fba
2instantiate Line 1801 in fd62fba
3instantiate the initializer for Line 1811 in 9d5b9de
4instantiate function Line 1669 in fd62fba
5instantiate alias template Line 1670 in fd62fba
6instantiate class template Line 1468 in fd62fba
The compiler forms the declarations for all member functions. That involves Line 1368 in fd62fba
7Overload resolution looks at all the currently declared map member functions. Line 1352 in fd62fba
template <typename T,
FMT_ENABLE_IF(
std::is_constructible<basic_string_view<char_type>, T>::value &&
!is_string<T>::value && !has_formatter<T, Context>::value &&
!has_fallback_formatter<T, char_type>::value)>
FMT_CONSTEXPR FMT_INLINE auto map(const T& val)
-> basic_string_view<char_type> {
return basic_string_view<char_type>(val);
} The constraint that causes all heck to break loose is !has_formatter<T, Context>::value 8Line 701 in fd62fba
// Specifies if T has an enabled formatter specialization. A type can be
// formattable even if it doesn't have a formatter e.g. via a conversion.
template <typename T, typename Context>
using has_formatter =
std::is_constructible<typename Context::template formatter_type<T>>; The formatter type is 9fmtlib has 23 partial specializations for Line 581 in fd62fba
template <typename T, typename Char>
struct formatter<
T, Char,
enable_if_t<
fmt::is_range<T, Char>::value
// Workaround a bug in MSVC 2019 and earlier.
#if !FMT_MSC_VER
&& (is_formattable<detail::value_type<T>, Char>::value ||
detail::has_fallback_formatter<detail::value_type<T>, Char>::value)
#endif
>> { The Line 1787 in fd62fba
template <typename T, typename Char = char>
using is_formattable = bool_constant<
!std::is_base_of<detail::unformattable,
decltype(detail::arg_mapper<buffer_context<Char>>().map(
std::declval<T>()))>::value &&
!detail::has_fallback_formatter<T, Char>::value>; The 10This is the problem with incomplete and the equivalence of alias template specializations. The standard says that alias template specializations are equivalent for the purpose of redeclarations: template<typename T> // #1
f(alias<T>);
// Other stuff!
// This must be
template<typename T> // #2
f(alias<T>); #2 must match #1, even though "other stuff" could change what this alias specialization actually resolves to. So where is the equivalence of alias template specializations expected? Nobody really knows. Certainly for redeclarations. I was hoping it would hold generally, because that really speeds up build times. I've never had a problem until this |
Thanks a lot for the detailed explanation! |
fmtlib has thrown me for a huge loop. I have an issue related to caching of alias template specializations. After a lot of hunting, I determined it's caused by this trailing return type during class template instantiation:
fmt/include/fmt/core.h
Lines 1360 to 1361 in 9d5b9de
The return type is calling
this->map("")
, but this (arg_mapper
) is still an incomplete type - I've only instantiated the functions above themap
at line 1360 at this point.When overload resolution considers the constructor at line 1345, the
std::is_constructible<>
constraint on 1342 ends up (many levels of indirection down) attempting to construct an instance ofarg_mapper
in an unevaluated context. However, this attempt fails, because no default ctor is declared because I haven't finished injecting all the member functions, and therefore no implicit declaration of the default ctor.The problem is that this lack of ctor causes an alias template to fail substitution, and I've noted that failure in my alias template cache. After the
arg_mapper
class is complete, there's anotheris_formattable
evaluation on the same arguments. This time it should succeed, but I've already cached it is a SFINAE failure, so am returning that, which causes the range test to fail./usr/include/c++/10/type_traits:906:5 - is_constructible
/usr/include/c++/10/type_traits:900:5 - __is_constructible_impl
Here the __is_constructible compiler builtin is invoked with
T = formatter<char[1], char, void>
The partial specializations of
formatter
are examined, since one must be selected in order to consider construction. The ranges partial makes things go haywire:fmt/include/fmt/ranges.h
Line 581 in 9d5b9de
This guy is in the list of constraints:
That alias template is defined like so:
fmt/include/fmt/core.h
Lines 1776 to 1781 in 9d5b9de
The compiler attempts to construct
arg_mapper
in this unevaluated context:However,
arg_mapper
is still incomplete, because we're still trying to evaluate the trailing-return-type for themap
functions. Therefore, the implicit default ctor hasn't yet been declared.__is_constructible
returns false, since there is no yet-declared ctor, and the alias template specializationis_formattable
gets its SFINAE failure written to the alias template cache.This substitution failure doesn't effect
arg_mapper
's definition at all. All it did was knock this rangesformatter
partial out of the candidate set, but that wasn't going to be the partial chosen, since there are character formatters that are chosen.fmt/include/fmt/ranges.h
Line 581 in 9d5b9de
The problem is that later on we really do need this partial template definition of
formatter
:fmt/test/enforce-checks-test.cc
Line 55 in 9d5b9de
The
vector
formatter is supposed to match it. But we've already attempted evaluatedis_formattable
on those same arguments, and it failed, so we just use that cached value, and this time the program is ill-formed, since there is no partial candidate.You have a note about an MSVC workaround at this place:
fmt/include/fmt/ranges.h
Line 585 in 9d5b9de
Was there a discussion about this? I don't know if this is a compiler bug or a library bug. Should two specializations of the same alias template with the same arguments be considered equivalent?
Either way, I think it's very dangerous to be writing
this->map("")
in your trailing-return-type, since thearg_mapper
class is incomplete, you're not getting an accurate picture of the class.FWIW, I've never experienced a break like this with Hana, Boost.PFR, mdspan or the other really advanced template libraries. I think it's cursed code.
If we removed the trailing-return-type from those
map
overloads and rely on return type deduction, everything works:Since it's in the body of the function, it's not instantiated until after the
arg_mapper
specialization is made complete, so thearg_mapper
implicit ctor is defined.;tldr:
Can we not do
decltype(this->map(""))
in the return type and instead just name the type?The text was updated successfully, but these errors were encountered: