Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implicit conversion when overloading operators for template classes

I would like to know why implicit type conversion doesn't work with outside operator overloading on class templates. Here is the working, non-templated version:

class foo
{
public:

    foo() = default;

    foo(int that)
    {}

    foo& operator +=(foo rhs)
    {
        return *this;
    }
};

foo operator +(foo lhs, foo rhs)
{
    lhs += rhs;
    return lhs;
}

As expected, the following lines compile correctly:

foo f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // OK
f = 5 + f; // OK

On the other hand, when class foo is declared as a simple template like this:

template< typename T >
class foo
{
public:

    foo() = default;

    foo(int that)
    {}

    foo& operator +=(foo rhs)
    {
        return *this;
    }
};

template< typename T >
foo< T > operator +(foo< T > lhs, foo< T > rhs)
{
    lhs += rhs;
    return lhs;
}

The following lines compile with errors:

foo< int > f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // Error (no match for operator+)
f = 5 + f; // Error (no match for operator+)

I would like to understand why the compiler (GCC 4.6.2) is unable to perform implicit type conversion using the converting constructor for the template version of the class. Is that the expected behaviour? Apart from manually creating all the necessary overloads, is there any workaround for this?

like image 862
pmjobin Avatar asked Jan 17 '12 05:01

pmjobin


3 Answers

The reason it does not just work is that implicit type conversions (that is, via constructors) do not apply during template argument deduction. But it works if you make the outside operator a friend since then the type T is know, allowing the compiler to investigate what can be casted to make the arguments match.

I made an example based on yours (but removed C++11 stuff), inspired by Item 46 (a rational number class) in Scott Meyers Effective C++ (ed 3). Your question is almost an exact match to that item. Scott also notes that ... "this use of friend is not related to the access of non-public parts of the class."

This will also allow work with mixes of foo< T >, foo< U > etc as long as T and U can be added etc.

Also look at this post: C++ addition overload ambiguity

#include <iostream>

using namespace std;

template< class T >
class foo
{
private:
   T _value;
public:
   foo() : _value() {}

   template <class U>
   foo(const foo<U>& that) : _value(that.getval()) {}

   // I'm sure this it can be done without this being public also;
   T getval() const { return _value ; }; 

   foo(const T& that) : _value(that) {}

   friend const foo operator +(foo &lhs,const foo &rhs) 
      {
     foo result(lhs._value+rhs._value); 
     return result;
      };
   friend const foo operator +(foo &lhs,const T &rhsval) 
      {
     foo result(lhs._value+rhsval); 
     return result;
      };
   friend const foo operator +(const T &lhsval,foo &rhs) 
      {
     foo result(lhsval+rhs._value); 
     return result;
      };

   friend foo& operator +=(foo &lhs,const foo &rhs)
      {
     lhs._value+=rhs._value;
     return lhs;
      };   
   friend std::ostream& operator<<(std::ostream& out, const foo& me){
      return out <<me._value;
   }
};

int main(){
   foo< int > f, g;
   foo< double > dd;
   cout <<f<<endl;
   f = f + g;
   cout <<f<<endl;
   f += 3 ;
   cout <<f<<endl;
   f = f + 5;
   cout <<f<<endl;
   f = 7 + f; 
   cout <<f<<endl;      
   dd=dd+f;
   cout <<dd<<endl;      
   dd=f+dd;
   cout <<dd<<endl;      
   dd=dd+7.3;
   cout <<dd<<endl;             
}
like image 85
Johan Lundberg Avatar answered Oct 18 '22 20:10

Johan Lundberg


I put this question to the library authors at MS and got an extremely informative response from Stephan Lavavej, so I give him full credit for this information.

The compile error you get in the template case is due to the fact that template argument deduction runs before overload resolution, and template argument deduction needs exact matches to add anything to the overload set.

In detail, template argument deduction looks at each pair of parameter type P and argument type A, and tries to find template substitutions that will make A exactly match P. After finding matches for each argument, it checks for consistency (so that if you call bar(foo<T>, foo<T>) with T=int for the first parameter and T=double as the second, it also fails). Only after exact, consistent matches are successfully substituted in the function signature is that signature added to the set of candidate functions for overload resolution.

Only after all ordinary functions (found through name lookup) and matching function template signatures have been added to the overload set is overload resolution run, at which point all of these function signatures are evaluated for a "best match", during which time implicit conversions will be considered.

For the operator+(foo<T>, foo<T>) case with foo<int> + 5, template argument deduction can find no substitution for T that will make the expression foo<T> exactly match int, so that overload of operator+ gets tossed out as a candidate and the implicit conversion is never even seen.

The opinion here seems to be that this is generally a good thing, as it makes templates much more predictable, leaving the realm of strange implicit behaviors to overload resolution.

The standard has plenty to say about this at:

14.8.2.1 Deducing template arguments from a function call

"Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below. ...

... In general, the deduction process attempts to find template argument values that will make the deduced A identical to A (after the type A is transformed as described above)"

It goes on to list a few special cases where this rule has exceptions involving cv-qualifiers (so T& will be compatible with const T&), and matching of derived classes (it can in some cases match Derived& to Base&) but basically, exact matching is the rule.

like image 30
brendanw Avatar answered Oct 18 '22 20:10

brendanw


All possible foo<T> are equally valid conversions from int since the constructor takes int, not the template type. The compiler isn't able to use the other parameter in the operator to guess which one you might mean, so you get the error. If you explicitly tell it which instantiation you want I believe it would work.

like image 22
Mark B Avatar answered Oct 18 '22 22:10

Mark B