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;
}
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;
}
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;
}