views:

536

answers:

4

Hello,

I am learning C++ and I was wondering if I could gain some insight into the preferred way of creating binary operators that work on instances of two different types. Here is an example that I've made to illustrate my concerns:

class A;
class B;

class A
{
    private:
     int x;

    public:
     A(int x);

     int getX() const;

     int operator + (const B& b);
};


class B
{
    private:
     int x;

    public:
     B(int x);

     int getX() const;

     int operator + (const A& A);
};


A::A(int x) : x(x) {}

int A::getX() const { return x; }

// Method 1
int A::operator + (const B& b) { return getX() + b.getX(); }


B::B(int x) : x(x) {}

int B::getX() const { return x; }

// Method 1
int B::operator + (const A& a) { return getX() + a.getX(); }


// Method 2
int operator + (const A& a, const B& b) { return a.getX() + b.getX(); }

int operator + (const B& b, const A& a) { return a.getX() + b.getX(); }


#include <iostream>

using namespace std;

int main()
{
    A a(2);
    B b(2);

    cout << a + b << endl;

    return 0;
};

If I would like to have symmetry among the two types, which method is the best approach in the above code. Are there any possible dangers in choosing one method over the other? Does this vary with the return type? Please explain! Thank you!

+2  A: 

The big risk with this approach is that people tend to perceive + as a symmetric operator. The way this is written, it is not (unless your implementations re the same).

At a minimum, you should overload + as an external binary operator (not as a member), and then play with overloading it several times.

You have to be careful, though, to make sure that nothing becomes ambiguous.

Can you explain what you're trying to do? I can't think of many cases of different types where it makes sense to have the symmetric heterogenous operators.

Uri
+7  A: 

The best way is to define (outside of either class) int operator+ (const A& a, const B& b), and make it a friend function of both classes if needed. In addition, define

int operator+(const B& b, const A& a) {return a + b;}

To make it symmetric.

rlbond
Make it a friend only if you have to. Sometimes you do and sometimes you don't.
David Thornley
A good point. Edited in.
rlbond
And hope that you don't have an implicit way to convert between A and B (e.g., constructors not marked explicit)...
Uri
+1  A: 

The main argument for method 2 is that you get implicit type conversion on both operands, not just the second one. This might save confusion somewhere down the line.

Speaking of which, your example code defines implicit conversions from int to A and from int to B, via the 1-arg constructors on both classes. This could result in ambiguity later. But if you left out the "explicit" for brevity, fair enough.

I agree with Uri's warning, though: if you find yourself doing this, you may be writing an API that others will find confusing. How come an A plus a B is an int? Does it really make things easier for users that they are adding a and b, rather than calling getX themselves and adding the results?

Is it because users know perfectly well that A and B are wrappers for ints? If so, then another option is to expose conversions from A to int and B to int, via operator int(). Then a+b will return an int for a sensible reason, and you'll get all the other arithmetic operators too:

#include <iostream>

struct A {
    int x;
    explicit A(int _x) : x(_x) {}
    operator int() {
        return x;
    }
};

struct B {
    int x;
    explicit B(int _x) : x(_x) {}
    operator int() {
        return x;
    }
};

int main() {
    A a(2);
    B b(2);
    std::cout << a + b << "\n";
    std::cout << a - b << "\n";
}
Steve Jessop
I created class A and B as examples. A more practical example might involve a Matrix and Vector class where multiplication is defined both ways. One way would give an ordinary vector and the other would assume as well as return a transposed vector.
Then ignore everything after my first paragraph :-)
Steve Jessop
What is the argument for making non-member operators friends of both classes? Is this just a convenience because you don't have to write "getter" methods, or is there some stylistic argument?
It should be made a friend only if it needs to be. Free non-friend functions aid encapsulation. But if it needs to be a friend, it doesn't totally ruin encapsulation to make it one, because a free function can still be part of the class interface. See for example http://www.gotw.ca/gotw/084.htm.
Steve Jessop
Btw, it was rlbond who initially said make it a friend, but it isn't needed for your A and B example as long as getX is public. For your matrix example, if you have public getters anyway and there's no efficiency gain by accessing internals, I'd say make that non-friend too.
Steve Jessop
I see. Thank you very much! I wish Stack Overflow had a better way of facilitating discussion than through these length-constrained comments.
A: 

I read in a comment that your intended use is adding vectors and matrices. Maybe you should consider using only matrices where vectors are one dimensional matrices. Then you are left with just one type and one set of operators:

matrix operator*( matrix const& a, matrix const& b );
matrix operator+( matrix const& a, matrix const& b ); // and so on

If you want to keep the vector class then you should consider whether you also want a transposed vector (maybe transpose is just an internal property of vector).

The set of operations is not really symmetric:

vector * matrix = vector
matrix * vector_t = vector_t
matrix * matrix = matrix
vector_t * vector = matrix
vector * vector_t = int

and you should offer those three operations (assuming transpose is a property of vector):

vector operator*( vector const& v, matrix const& m );
vector operator*( matrix const& m, vector const& v );
matrix operator*( matrix const& m1, matrix const& m2 );
matrix operator*( vector const& v1, vector const& v2 ); // possibly 1x1 matrix, you cannot overload changing only return value

All as free functions if possible. Even if the above set is not symmetric, neither is the real world and your users will expect it.

David Rodríguez - dribeas