Implicit Type Conversion With Template

Implicit type conversion with template

The solution is already shown in this answer. Now, more about the problem...

The problem in your code is how overload resolution is performed. When a template function is considered for overload resolution the compiler will perform type deduction on the arguments and come up with a type substitution that matches the call or else it fails to apply that template, removes it from the set of potential candidates and continues over. The problem at this point is that type deduction only deduces exact matches (with possible extra const/volatile qualification). Because the matching is exact, the compiler will not use any conversion (again, other than cv).

The simplest example of this happens with std::max and std::min functions:

unsigned int i = 0;
std::min( i, 10 ); // Error!

Type deduction will deduce the T in template <typename T> min( T const &, T const & ) to be unsigned for the first argument but int for the second they differ and the compiler will discard this template function.

The solution proposed in the answer is using a feature of the language that enables you to define a non-member friend function inside the class definition. The advantage with templates is that for every (different) instantiation of the template, the compiler will create a free non-template function at namespace level that has the signature obtained by substituting the real types of the instantiation in the friend declaration:

template <typename T>
class test {
friend test operator+( test const & lhs, test const & rhs ) { // [1]
return test();
}
}
test<int> t; // [2]

In the example above, the compiler allows you to add the definition of the friend function inside the class scope at [1]. Then when you instantiate the template in [2], the compiler will generate a free function:

test<int> operator+( test<int> const & lhs, test<int> const & rhs ) { 
return test<int>();
}

The function is defined always, whether you use it or not (this differs to the template class member functions, that are instantiated on demand).

The magic here has multiple sides to it. The first part is that it generically you are defining non-template functions for each and all of the instantiated types, so you gain genericity and at the same time the advantage of overload resolution being able to use this function when the arguments are not perfect matches.

Because it is a non-template function, the compiler is able to call implicit conversions on both arguments, and you will get your expected behavior.

Additionally, a different type of magic goes on with lookup, as the function so defined can only be found by argument dependent lookup unless it is also declared at namespace level, which in our case cannot be done in a generic way. The implication of this might be good or bad, depending on how you want to consider it...

Because it can only be found by ADL it will not be considered unless at least one of the arguments is already of the desired type (i.e. it will never be used performing conversions to both arguments). The downside is that it is impossible to refer to the function unless you are actually calling it, and that means that you cannot obtain a function pointer.

(More on template friendship here, but note that in this particular case, all the other variants will fail to perform implicit conversions).

Implicit type conversion with templated function parameters

Implicit conversions won't be considered in template argument deduction; the template parameter T just can't be deduced.

Type deduction does not consider implicit conversions (other than type adjustments listed above): that's the job for overload resolution, which happens later.

You can write a helper function template.

template <typename T>
void helper(T&& t) {
fy<std::decay_t<T>>(std::forward<T>(t)); // specify the template argument explicitly
}

then

helper(2); // T will be deduced as int

Allow implicit type conversion during template evaluation

The fault in this example lies with is_same. It declares that it requires two arguments of the same type, which is a requirement it does not need, and fails to require that type to have an ==, which it does need.

Granted, it is common to find C++ that poorly constrains template functions because it is difficult and verbose to do otherwise. Authors take a practical shortcut. That said, isn't the approach to fix the interface of is_same?

// C++17 version. Close to std::equal_to<>::operator().
template <typename T, typename U>
constexpr auto is_same(T&& t, U&& u)
noexcept(noexcept(std::forward<T>(t) == std::forward<U>(u)))
-> decltype(std::forward<T>(t) == std::forward<U>(u))
{
return std::forward<T>(t) == std::forward<U>(u);
}

With a corrected is_same, the code just works.

There are other examples one can imagine which may require two arguments to have the same type. For example, if the return type depends on the argument type and the return value can come from either:

template <typename T>
T& choose(bool choose_left, T& left, T& right) {
return choose_left ? left : right;
}

This is much rarer. But it might actually require thought to decide whether to use the underlying or wrapper type. If you have this enhanced behavior in the wrapper type, and conditionally use a wrapped value or an underlying value, should the underlying value be wrapped to continue to get the enhanced behavior, or do we drop the enhancement? Even if you could make this silently choose one of those two behaviors, would you want to?

However, you can still make it easier to get the value than to say static_cast<T>(...), for example by providing an accessor:

// given wrap<int> w and int i
is_same(w.value(), 5);
choose_left(true, w.value(), i);

I have a few other important comments:

wrap() : t() {}

This requires T be default constructible. = default does the right thing.

wrap(const T& _t) : t(_t) {}
wrap(T&& _t) : t(std::move(_t)) {}

These are not explicit. A T is implicitly convertible to a wrap<T> and vice versa. This does not work well in C++. For example, true ? w : i is not well-formed. This causes std::equality_comparable_with<int, wrap<int>> to be false, which would be a reasonable requirement for is_same. Wrapper types should probably be explicitly constructed, and can be implicitly converted to the underlying type.

constexpr operator T&() { return t; }
constexpr operator const T&() const { return t; }

These are not ref-qualified, so they return lvalue references even if the wrapper is an rvalue. That seems ill-advised.

Finally, construction and conversion only take into account the exact type T. But any place T is used, it might be implicitly converted from some other type. And two conversions are disallowed in C++. So for a wrapper type, one has a decision to make, and that often means allowing construction from anything a T is constructible from.

implicit conversion in template specialization

When compiler see this:

f('a', 1);

It is unable to deduce type since it has two choices:

f(const char &, const char &);
f(const int &, const int &);

Since your template has common type for both arguments.

Both choices are equally valid and there is no reasonable rule to resolve this ambiguity. So compiler has to report an error to avoid undesired behavior. Note that silent type conversion has no impact on this problem, also your specialization of template also can't help resolve this issue.

Same problem will be raised for std::max.

Now question is are you sure that second argument is more important and should have impact on template argument type? If yes then you can force to ignore type of first argument (disclaimer: this is unusual and unexpected so it may be a bug prone for future code mainteinrs).

template <typename T>
struct Identity {
using type = T;
};
// note C++20 introduces std::type_identity

template<typename T>
void f(const typename Identity<T>::type& x, const T& y)
{
std::cout << "generic " << x << " " << y << std::endl;
}

In this form first argument will not take a part in type deduction for template, since it depends on type of second argument.

Now this

f('a', 1);

will compile since second argument will lead to T=int and first argument type is expected to be same as for second argument. Now silent conversion can be performed.

Live example.

Why does the implicit type conversion not work in template deduction?

Because template argument deduction is not that smart: it does not (by design) consider user-defined conversions. And int -> Scalar<int> is a user-defined conversion.

If you want to use TAD, you need to convert your argument at the caller site:

func(a, Scalar<int>{2}); 

or define a deduction guide1 for Scalar and call f:

func(a, Scalar{2}); // C++17 only

Alternatively, you can explicitly instantiate f:

func<int>(a, 2); 

1) The default deduction guide is sufficient: demo.

Is there a way to force implicit conversion in function template argument deduction?

Command the template argument to be deduced from the vector, but not from the value

template<typename T> //                                             v      only required change       v
typename std::vector<T>::size_type count_(std::vector<T> const &vt, typename std::vector<T>::value_type const &value) {
std::cout << "count_(vector<T> const&, T const&)\n";
typename std::vector<T>::size_type n = 0;
for(auto const &e : vt) if(e == value) n++;
return n;
}

Template parameter deduction basically cannot "see" through type aliases, especially not ones that are members of an as-yet unknown specialization. The second argument to count_ no longer participates in deducing T. Rather, the vector always decides T, and then the second argument is implicitly converted to its type. Your specialization remains a specialization. Actually, it doesn't change.

// just cosmetic changes
template<>
std::vector<char const*>::size_type count_(std::vector<char const*> const &vcp, char const *const &value) {
std::cout << "count_(vector<char const*> const&, char const *const&)\n";
std::vector<char const*>::size_type n = 0;
for(auto const &e : vcp) if(!std::strcmp(e, value)) n++;
return n;
}

Godbolt

Note: in this case we're "lucky" that std::vector<T>::value_type is a conveniently available alias for T. In general, you can use std::type_identity_t<T> as a template deduction blocker, but that's only available in C++20. (Of course, you can implement it yourself—it's only two lines!)

Implicit conversion between template type

User-defined conversions are never considered when attempting to match up function argument types with function parameter types for template argument deduction. So this issue is a somewhat more complicated version of this sort of error:

template <int> struct X {};
struct Y {
operator X<2> () const { return {}; }
};
template <int N>
void f(X<N>) {}

int main()
{
Y y;
f(y); // Error: Cannot deduce template argument.
}

Since it seems you're making something along the lines of a template meta-programming library, perhaps you could define a custom mechanism for converting types in your library "to a template"?

#include <type_traits>

template<bool v>
struct bool_t {
// (Add some appropriate SFINAE.)
template<template<int> typename T>
constexpr T<v ? 1 : 0> convert_template() const {
return {};
}
};

template<typename T, template<int> class TT>
struct type_specializes : std::false_type {};

template<template<int> class TT, int N>
struct type_specializes<TT<N>, TT> : std::true_type {};

template<int N>
struct number_t {
// Allow "conversion" to my own template:
template<template<int> typename T>
constexpr std::enable_if_t<type_specializes<number_t, T>::value, number_t>
convert_template() const { return {}; }

private:
// Used only in decltype; no definition needed.
template<int n1, int n2>
static number_t<n1 + n2> sum_impl(number_t<n1>, number_t<n2>);

template<typename T1, typename T2>
friend auto operator+(T1&& x, T2&& y)
-> decltype(number_t::sum_impl(
x.template convert_template<number_t>(),
y.template convert_template<number_t>()))
{ return {}; }
};

int main() {
number_t<0>{} + bool_t<0>{};
}

If you want to allow both operands to be bool_t specializations, the operator+ will need to be a proper visible namespace member; you can add more SFINAE checks to it if appropriate.

template deduction and implicit constructors

Template argument deduction does not consider any potential type conversions - mainly because deduction happens before any such conversions can happen.

Basically, when the compiler sees fun(a), it first gathers a set of foo functions that are eligible to service that call. If a foo function template is found, the compiler tries to generate a concrete function from it by substituting the template arguments with the types of arguments passed in the call statement. That's where the template argument deduction happens. No type conversion can happen here because there is no concrete type to convert to. In your example, B<T> is not a type to convert to because T is unknown and it cannot be discovered from an argument of type A<int>. It is also not known whether an arbitrary instance of B<T> will be constructible from A<int> because different specializations of B may have different sets of constructors. Hence the deduction fails in your case.

If the deduction succeeds, the concrete function (with the deduced argument types) is added to the candidate set.

When the set of candidates is gathered, then the best matching candidate is chosen. At this point argument conversions are considered. The conversions are possible since the candidates are no longer templates, and the target types for conversions are known.

What you could do to work around it is to allow the compiler to deduce the template as well and then construct B<T> explicitly:

template<typename... Ts> void fun_impl(B<Ts>...);

template<template<typename> class X, typename... Ts>
std::enable_if_t<(std::is_constructible_v<B<Ts>, X<Ts>> && ...)> fun(X<Ts>... args)
{
fun_impl(B<Ts>(args)...);
}

int main()
{
A<int> a;
fun(B(a)); // works, instantiates fun<B, int>
fun(a); // works, instantiates fun<A, int>
}


Related Topics



Leave a reply



Submit