Home > Software engineering >  Why do implicit conversions not work for out-of-line operators on a template class
Why do implicit conversions not work for out-of-line operators on a template class

Time:09-01

I tried writing a class with operators two different ways. First, I tried it with the operators defined inside the class. Then, I tried it with the operators defined outside the class. Defining the operators outside the class appears to be better because I can take advantage of implicit conversions on the left-hand-operand. It appears to be widely recommended that operators be defined outside the class when possible.

However, when I make the class a template class, implicit conversions on the left-hand-operand no longer work. In fact, implicit conversions on the right-hand-operand also do not work. Am I doing something wrong?

namespace N1 {
template <class T>
class C1 {
 public:
  double value;
  /* implicit */ C1(double value) : value{value} {}
  C1(const C1<T>& other) : value{other.value} {}
  C1<T>& operator=(const C1<T>& other) {
    this->value = other.value;
    return *this;
  }
  // Define operator  for C1 inline
  inline C1<T> operator (const C1<T>& other) const {
    return this->value   other.value;
  }
};

template <class T>
class C2 {
 public:
  double value;
  /* implicit */ C2(double value) : value{value} {}
  C2(const C2<T>& other) : value{other.value} {}
  C2<T>& operator=(const C2<T>& other) {
    this->value = other.value;
    return *this;
  }
};
// Define operator  for C2 out-of-line
template <class T>
inline C2<T> operator (const C2<T>& self, const C2<T>& other) {
  return self.value   other.value;
}
} // namespace N1

namespace {
using C1 = N1::C1<int>;
using C2 = N1::C2<int>;

// double f1(double x, const C1& y) {
//   return (x   y).value; // not expected to work
// }
double f2(const C1& x, double y) {
  return (x   y).value; // works
}
double f3(double x, const C2& y) {
  // Works when C2 is a class, fails when C2 is a template class
  return (x   y).value;
}
double f4(const C2& x, double y) {
  // Works when C2 is a class, fails when C2 is a template class
  return (x   y).value;
}
} // namespace

my hope is that clients should be able to write code such as

void my_main() {
  N1::C2<Anything> x{4};
  auto y = x   2.0;
  auto z = 2.0   x;
}

CodePudding user response:

You can fix C1's first test case by adding the out of line operator

template<class T>
C1<T> operator (double d, const C1<T>& c1) {
    return c1   d;
}

to get both test cases to pass:

double f1(double x, const C1& y) {
   return (x   y).value; // not expected to work - but now works
}
double f2(const C1& x, double y) {
    return (x   y).value;  // works
}

The C2 tests: To fix those, you need to make N1::C2<int> a dependent type. You can do that with a bit of SFINAE which considers all N1::C2<T>s, but only accepts it when T is int. Note that it's a problem with the test cases - not with implicit conversion.

template<class T>
std::enable_if_t<std::is_same_v<T,int>, double>
f3(double x, const N1::C2<T>& y) {
    return (x   y).value; // implicit conversion works
}

template<class T>
std::enable_if_t<std::is_same_v<T,int>, double>
f4(const N1::C2<T>& x, double y) {
    return (x   y).value; // implicit conversion works
}

or using your C2 typedef which makes it consider all T's but only accepts N1::C2<int>:

using C2 = N1::C2<int>;

template<class T>
std::enable_if_t<std::is_same_v<T,C2>, double>
f3(double x, const T& y) {
    return (x   y).value; // implicit conversion works
}

template<class T>
std::enable_if_t<std::is_same_v<T,C2>, double>
f4(const T& x, double y) {
    return (x   y).value; // implicit conversion works
}

In the comments you say you want the functions to accept all N1::C2<T>s and then it becomes simpler:

template<class T>
auto f3(double x, const N1::C2<T>& y) {
    return (x   y).value;
}

template<class T>
auto f4(const N1::C2<T>& x, double y) {
    return (x   y).value;
}
  • My hope is that clients can write code such as ... Is that not possible?
    N1::C2<int> x{4};
    
    auto y = x   2.0;
    auto z = 2.0   x;
    

Yes, but you then need to add overloads for that:

template <class T>
C2<T> operator (const C2<T>& lhs, double rhs) {
    return lhs.value   rhs;
}

template <class T>
C2<T> operator (double lhs, const C2<T>& rhs) {
    return rhs   lhs; // just swapped the order to use the above
}

Another option, which is how it's commonly done, is to add the operator = member function:

template<class T>
class C2 {
// ...
    C2& operator =(const C2& other) {
        value  = other.value;
        return *this;
    }
};

You could then define the free functions like so:

template <class T>
C2<T> operator (const C2<T>& lhs, std::convertible_to<C2<T>> auto&& rhs) {
    auto rv = lhs;
    rv  = rhs;
    return rv;
}

template <class T, class U>
std::enable_if_t<!std::same_as<std::decay_t<U>*, C2<T>*>, C2<T>>
operator (const U& lhs, const C2<T>& rhs) {
    return rhs   lhs;
}

Demo

Instead of SFINAE as above, you could create a home made concept to avoid ambiguity when adding two C2<T>s:

template <class From, class To>
concept convertible_to_but_is_not =
    not std::same_as<std::remove_reference_t<From>, To> &&
    std::convertible_to<From, To>;

template <class T>
C2<T> operator (const C2<T>& lhs, std::convertible_to<C2<T>> auto&& rhs) {
    auto rv = lhs;
    rv  = rhs;
    return rv;
}

template <class T> // making sure that lhs is not a C2<T>
C2<T> operator (convertible_to_but_is_not<C2<T>> auto&& lhs, const C2<T>& rhs) {
    return rhs   lhs;
}

Demo

CodePudding user response:

Per Ted's answer:

Implicit conversions are not considered from function arguments during template argument deduction. This problem can be addressed by the addition of two helper functions:

template <class T>
inline C2<T> operator (double self, const C2<T>& other) {
  return self   other.value;
}
template <class T>
inline C2<T> operator (const C2<T>& self, double other) {
  return self.value   other;
}
  • Related