How to prefer implicit conversion over template argument resolution?

Viewed 139

Imagine the following code snippet:

#include <string>
#include <string_view>

template<class T> void foo(std::basic_string_view<T> v) {
    // Some work
}

void bar(std::string_view v) {
    // Some other work
}

int main() {
    std::string x = "abc";
    
    foo<char>(x); // OK
    bar(x);       // OK
    foo(x);       // error: std::__cxx11::basic_string<char> is not derived from std::basic_string_view<T>

    return 0;
}

Note, that std::basic_string<CharT, Traits, Allocator> has operator std::basic_string_view<CharT, Traits>() const noexcept for consistent template arguments, therefore bar(x) and foo<char>(x) are ok.

In foo, I want to implement a generic algorithm for any character type. However, foo has to be a template function, and T cannot be derived correctly, because implicit type conversions are not respected during template argument type deduction.

The question is the following. Is there any C++ template meta-programming magic to make the last line compiling in a most generic way? I mean that partial solution would be to write a wrapper:

template<class T> void foo(const std::basic_string<T>& v) {
    foo(static_cast<std::basic_string_view<T>>(v));
}

but this won't work for other user types convertible to std::basic_string_view<T>.

2 Answers

In a general way no, it's not possible because you don't know the type T for which an arbitrary class X is convertible to std::basic_string_view<T> and there can be multiple T types such that X is convertible to std::basic_string_view<T>.

However we can look for usual patterns such that the class X has a typedef value_type and X is convertible to std::basic_string_view<typename X::value_type>. This is far from ideal but depending on your needs it could cover a reasonable amount of cases:

template <class T>
void foo(std::basic_string_view<T>) {}

template <class T,
    std::enable_if_t<
        std::is_convertible_v<const T&, std::basic_string_view<typename T::value_type>>,
        int> = 0>
void foo(const T& v)
{
    return foo(std::basic_string_view<typename T::value_type>{v});
}

int main()
{
    std::string x = "abc";

    foo<char>(x); // OK
    foo(x);
}

C++20 concepts:

template <class T>
void foo(std::basic_string_view<T>) {}

template <class T>
    requires std::convertible_to<const T&, std::basic_string_view<typename T::value_type>>
void foo(const T& v)
{
    return foo(std::basic_string_view<typename T::value_type>{v});
}

If you really are intent on making this as generic as possible we can use the fact that there are only a handful of character types so the pool of std::basic_string_view<CharT> is relatively narrow and overload for all of them (simple overloads suffice for this):

template <class T>
void foo(std::basic_string_view<T>)
{
}

void foo(std::string_view v)    { return foo<char>(v); }
void foo(std::wstring_view v)   { return foo<wchar_t>(v); }
void foo(std::u8string_view v)  { return foo<char8_t>(v); }
void foo(std::u16string_view v) { return foo<char16_t>(v); }
void foo(std::u32string_view v) { return foo<char32_t>(v); }

Note that this will fail if a class T is convertible to multiple basic_string_view classes, but I don't see a use case for such a class.

This is not possible in general. When you write:

template<class T> void foo(std::basic_string_view<T> v) {
    // Some work
}

What we want is for foo to take an input (of some type U, let's say) for which there exists a T such that U is convertible to std::basic_string_view<T>. This kind of existential type is not something which C++ has any support for.


If we break from generality, though, we can support this. All we need is some way to figure out the character type of the type we are converting to std::basic_string_view.

For example, we can support any range which is convertible to a std::basic_string_view<CharT>:

template<
    class T,
    // In C++20, use std::range_value_t<T const>
    class CharT = std::decay_t<decltype(*std::begin(std::declval<T const&>()))>,
    std::enable_if_t<
        std::is_convertible_v<T const&, std::basic_string_view<CharT>>,
        int> = 0
    >
void foo(T const& t) {
    std::basic_string_view<CharT> v(t);
}

Instead of std::begin, you may want to support types which are ranges via using std::begin; begin(t), which requires more work, e.g. a CPO function to perform the two-step ADL for us.

We can also enable this to support non-ranges by adding a customization point that people can use to adapt their type to us:

template<class T, class = void> // second parameter to enable SFINAE
struct string_traits;

template<class T,
    class CharT = std::decay_t<decltype(*std::begin(std::declval<T const&>()))>>
struct string_traits<T, std::enable_if_t<
        std::is_convertible_v<T const&, std::basic_string_view<CharT>>>
    > {
    using char_type = CharT;
};

// Possibly specialized - this is an example which is unneeded
// due to the prior specialization:
template <class CharT>
struct string_traits<std::basic_string<CharT>> {
    using char_type = CharT;
};

template<
    class T,
    class CharT = typename string_traits<T>::char_type,
    std::enable_if_t<
        std::is_convertible_v<T const&, std::basic_string_view<CharT>>,
        int> = 0
    >
void foo(T const& t) {
    std::basic_string_view<CharT> v(t);
}
Related