#include <concepts>
#include <format>
#include <string_view>
#include <type_traits>
template <typename ... Args>
struct FmtString {
std::format_string<Args ...> text;
template <typename StringCompatible>
requires (std::convertible_to<StringCompatible, std::string_view>)
consteval FmtString(StringCompatible const& description)
: text(description)
{}
};
template <typename ... Args>
void fails(FmtString<Args ...> const&, Args&& ...) noexcept {}
template <typename... Args>
using FmtStringArgIdentity = FmtString<std::type_identity_t<Args> ...>;
template <typename ... Args>
void works(FmtStringArgIdentity<Args ...> const&, Args&& ...) noexcept {}
int main() {
works("test {}", 42);
fails("test {}", 42);
}
GCC error message:
<source>: In function 'int main()':
<source>:28:10: error: no matching function for call to 'fails(const char [8], int)'
28 | fails("test {}", 42);
| ~~~~~^~~~~~~~~~~~~~~
<source>:18:6: note: candidate: 'template<class ... Args> void fails(const FmtString<Args ...>&, Args&& ...)'
18 | void fails(FmtString<Args ...> const&, Args&& ...) noexcept {}
| ^~~~~
<source>:18:6: note: template argument deduction/substitution failed:
<source>:28:10: note: mismatched types 'const FmtString<Args ...>' and 'const char [8]'
28 | fails("test {}", 42);
| ~~~~~^~~~~~~~~~~~~~~
Compiler returned: 1
Looking on the source code of libfmt I was able to make my code work. Unfortunately, I don't understand why this works or why the template argument deduction fails.
What is the problem with the fails
call and why does the works
call solve this?
Here is a simpler example with the same effect:
#include <type_traits>
template <typename T>
struct foo {
template <typename U>
foo(U u) {}
};
template <typename T>
void fails(foo<T>,T) {}
template <typename T>
void works(std::type_identity_t<foo<T>>,T) {}
int main()
{
fails(12.0,42); // error
works(12.0,42);
}
You cannot deduce int
from 12.0
that would be used to convert to a foo<int>
. There is simply no relation between double
used as template argument to the constructor and int
used to instantiate foo<int>
. Any foo<T>
has a converting constructor from double
to foo<T>
.
std::type_identity
is a non-deduced context. For works<T>
, the T
is only deduced from 42
, and works<int>
is instantiated to get works<int>(foo<int>,int)
. Only now the implicit conversion kicks in and 12.0
can be converted to foo<int>
via the converting constructor.