views:

128

answers:

6

I'm having trouble with the inheritance of operator=. Why doesn't this code work, and what is the best way to fix it?

#include <iostream>

class A
{
public:
    A & operator=(const A & a)
    {
        x = a.x;
        return *this;
    }

    bool operator==(const A & a)
    {
        return x == a.x;
    }

    virtual int get() = 0; // Abstract

protected:
    int x;
};

class B : public A
{
public:
    B(int x)
    {
        this->x = x;
    }

    int get()
    {
        return x;
    }
};

class C : public A
{
public:
    C(int x)
    {
        this->x = x;
    }

    int get()
    {
        return x;
    }
};

int main()
{
    B b(3);
    C c(7);
    printf("B: %d C: %d B==C: %d\n", b.get(), c.get(), b==c);

    b = c; // compile error
    // error: no match for 'operator= in 'b = c'
    // note: candidates are B& B::operator=(const B&)

    printf("B: %d C: %d B==C: %d\n", b.get(), c.get(), b==c);
    return 0;
}
+1  A: 

What's happening is that the default operator = that the compiler generates for any class that doesn't have one is hiding the base class' operator =. In this particular case, the compiler is generating const B &B::operator =(const B &) for you behind the scenes. Your assignment matches this operator and completely ignores the one you declared in class A. Since a C& cannot be converted to a B& the compiler generates the error you see.

You want this to happen, even though it seems vexing right now. It prevents code like you've written from working. You don't want code like that to work because it allows unrelated types (B and C have a common ancestor, but the only important relationships in inheritance are parent->child->grandchild relationships, not sibling relationships) to be assigned to one another.

Think about it from an ISA perspective. Should a Car be allowed to be assigned to a Boat just because they're both Vehicles?

In order make something like this work you should use the Envelope/Letter pattern. The envelope (aka handle) is a specialized class who's only job it is is to hold an instance of some class that's derived from a particular base class (the letter). The handle forwards all operations but assignment to the contained object. For assignment it simply replaces the instance of the internal object with a copy-constructed (using a 'clone' method (aka virtual constructor)) copy of the assigned from object.

Omnifarious
It's not at all unreasonable to want to copy a base subobject from one derived class to another. Consider creating an Order from a Quote. They aren't the same type, but they share a whole bunch of information about the Client (name/address/etc) and ordered products, yet neither is a true superset of the other (order has no quote expiration date, quote has no billing information). Of course, the function to so this should be a normal function, not `operator=`, and then there'd not be any compiler-generated version hiding it from derived classes.
Ben Voigt
+1  A: 

You cannot assign across the hierarchy like this - B and C are different subclasses of A. You can assign a B to a B or a C to a C but not a C to a B or vice versa.

You probably want to implement operator= in B and C, delegating the A part of the assignment to A::operator= before you try this though. Otherwise the B- and C-specific parts of those classes will get lost in the assignment.

Steve Townsend
That is not really the reason the code is failing.
Omnifarious
+1  A: 

= operator is not inherited in C++!

Als
Not true. Operator `=` *is* inherited in C++. Yet, its name is *hidden* by he implicitly declared `operator =` in the derived class.
AndreyT
@AndreyT : Excellent comment. I get your point! just that they never teach it this way and then as we go on I never thought about it in this sense. But makes perfect sense now, Thank You!
Als
+1  A: 

Normally, operator= is defined in B as

B& operator=(B const &);

Since B is not an unambiguous and accessible base of 'C', the conversion from C to B is not allowed by the compiler.

If you really want to have a 'C' be assigned to 'B', 'B' should support an appropriate assignment operator as

B& operator=(C const &);
Chubsdad
+10  A: 

If you do not declare copy-assignment operator in a class, the compiler will declare one for you implicitly. The implicitly declared copy-assignment operator will hide any inherited assignment operators (read about "name hiding" in C++), meaning that any inherited assignment operators will become "invisible" to the compiler, unless you take specific steps to "unhide" them.

In your case, class B has no explicitly declared copy-assignment operator. Which mean that the compiler will declare

B& B::operator =(const B&)

implicitly. It will hide the operator inherited from A. The line

b = c;

does not compile, because, the only candidate here is the above implicitly declared B::operator = (the compiler told you about that already); all other candidates are hidden. And since c is not convertible to B&, the above assignment does not compile.

If you want your code to compile, you can use using-declaration to unhide the inherited A::operator = by adding

using A::operator =;

to the definition of class B. The code will now compile, although it won't be a good style. You have to keep in mind that in this case the b = c assignment will invoke A::operator =, which assigns only the A portions of the objects involved. (But apparently that is your intent.)

Alternatively, in cases like this you can always work around name hiding by using a fully qualified version of the name

b.A::operator =(c);
AndreyT
+1  A: 

(Probably not a fix & probably not what you should do) BUT... there's a way you can force the issue if you really must:

 (A&)(*(&b)) = (A&)(*(&c))
slashmais