Search code examples
c++type-conversionc++20implicit-conversionc++-templates

Why do I need `std::type_identity_t` to enable implicit type conversion here?


#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?

Live-Code


Solution

  • 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.