Detect assignment of base class to reference pointing at derived class
Asked Answered
B

1

6

I'm currently investigating the interplay between polymorphic types and assignment operations. My main concern is whether or not someone might try assigning the value of a base class to an object of a derived class, which would cause problems.

From this answer I learned that the assignment operator of the base class is always hidden by the implicitely defined assignment operator of the derived class. So for assignment to a simple variable, incorrect types will cause compiler errors. However, this is not true if the assignment occurs via a reference:

class A { public: int a; };
class B : public A { public: int b; };
int main() {
  A a; a.a = 1;
  B b; b.a = 2; b.b = 3;
  // b = a; // good: won't compile
  A& c = b;
  c = a; // bad: inconcistent assignment
  return b.a*10 + b.b; // returns 13
}

This form of assignment would likely lead to inconcistent object state, however there is no compiler warning and the code looks non-evil to me at first glance.

Is there any established idiom to detect such issues?

I guess I only can hope for run-time detection, throwing an exception if I find such an invalid assignment. The best approach I can think of just now is a user-defined assigment operator in the base class, which uses run-time type information to ensure that this is actually a pointer to an instance of base, not to a derived class, and then does a manual member-by-member copy. This sounds like a lot of overhead, and severely impact code readability. Is there something easier?

Edit: Since the applicability of some approaches seems to depend on what I want to do, here are some details.

I have two mathematical concepts, say ring and field. Every field is a ring, but not conversely. There are several implementations for each, and they share common base classes, namely AbstractRing and AbstractField, the latter derived from the former. Now I try to implement easy-to-write by-reference semantics based on std::shared_ptr. So my Ring class contains a std::shared_ptr<AbstractRing> holding its implementation, and a bunch of methods forwarding to that. I'd like to write Field as inheriting from Ring so I don't have to repeat those methods. The methods specific to a field would simply cast the pointer to AbstractField, and I'd like to do that cast statically. I can ensure that the pointer is actually an AbstractField at construction, but I'm worried that someone will assign a Ring to a Ring& which is actually a Field, thus breaking my assumed invariant about the contained shared pointer.

Barnard answered 28/8, 2014 at 10:53 Comment(9)
Isn't the real issue here that you have a non-abstract base class?Memberg
Personally I disable copy constructor and assignment operators for polymorphic types. Inheritance base polymorphism really doesn't play well with value types.Haver
@OliCharlesworth: I don't see how. Think of e.g. a toggle-button derived from a button, I can see situations where the base class should be instantiable and the issue might arise in real world. Therefore I'd not follow any “all bases must be abstract” approach. If this is not what you had in mind, then please elaborate how abstract base classes can help with my situation.Barnard
@quantdev: Now, I'm not binding a reference, I'm assigning a value. The binding was the line before, and C++ doesn't allow reference rebinding.Barnard
Maybe you should use composition rather than inheritance? Hard to tell from the general example given.Kittle
@C.R.: Deleting copy constructors is easy to write, so worth considering. I'd welcome that as an answer so it can collect votes. I fear that in my current setup, I'd need assignment. I'll post some details about my current application in a moment.Barnard
@MvG: Yeah, that's what I had in mind. I'd still argue that's an issue with the class hierarchy, even in the example you gave (although of course, in many situations, you have no choice as you're working with an existing API...)Memberg
Putting a Ring inside Field rather than inheriting it would still give you access to the forwarding methods of Ring. They seem like pretty simple concepts... are you sure you're not overdesigning it?Kittle
@Zoomulator: From a semantic point of view, a field is a ring, not contained in one. So if I have some Field f, it would feel natural to write f.getZeroElement() but unnatural to write f.ring.getZeroElement(). Since math is hard enough to read without taking peculiarities of the programming language into account, I'd try very hard to accommodate the former syntax, but not having to implement getZeroElement() { return this->ring.getZeroElement(); } as a method of Field would be good.Barnard
K
1

Since the assignment to a downcast type reference can't be detected at compile time I would suggest a dynamic solution. It's an unusual case and I'd usually be against this, but using a virtual assignment operator might be required.

class Ring {
    virtual Ring& operator = ( const Ring& ring ) {
         /* Do ring assignment stuff. */
         return *this;
    }
};

class Field {
    virtual Ring& operator = ( const Ring& ring ) {
        /* Trying to assign a Ring to a Field. */
        throw someTypeError();
    }

    virtual Field& operator = ( const Field& field ) {
        /* Allow assignment of complete fields. */
        return *this;
    }
};

This is probably the most sensible approach.

An alternative may be to create a template class for references that can keep track of this and simply forbid the usage of basic pointers * and references &. A templated solution may be trickier to implement correctly but would allow static typechecking that forbids the downcast. Here's a basic version that at least for me correctly gives a compilation error with "noDerivs( b )" being the origin of the error, using GCC 4.8 and the -std=c++11 flag (for static_assert).

#include <type_traits>

template<class T>
struct CompleteRef {
    T& ref;

    template<class S>
    CompleteRef( S& ref ) : ref( ref ) {
        static_assert( std::is_same<T,S>::value, "Downcasting not allowed" );
        }

    T& get() const { return ref; }
    };

class A { int a; };
class B : public A { int b; };

void noDerivs( CompleteRef<A> a_ref ) {
    A& a = a_ref.get();
}

int main() {
    A a;
    B b;
    noDerivs( a );
    noDerivs( b );
    return 0;
}

This specific template can still be fooled if the user first creates a reference of his own and passes that as an argument. In the end, guarding your users from doing stupid things is an hopeless endeavor. Sometimes all you can do is give a fair warning and present a detailed best-practice documentation.

Kittle answered 28/8, 2014 at 13:4 Comment(5)
Interesting. “assignment to a downcast type reference can't be detected at runtime”: in C++11 you can do typeid(*this) == typeid(Ring) inside the Ring::operator=(…) implementation, which is what I had in mind. Not sure whether the virtual operator has better or worse performance, would have to test that one day. “This is the standard way of assignment in java.” But Java has reference semantics, so I don't see what kind of assignment you are referring to here. Assignment of array members comes close but still looks different to me. That Java aspect is probably off topic, but I'm curious.Barnard
@Barnard The typeid comparison is done at runtime using RTTI. The virtual assignment uses the class virtual table and requires no comparison. Generally virtual members are preferred in c++, but I'm not sure if RTTI gives significant overhead in comparison. The virtual call is a -single- pointer indirection, while RTTI may involve more (not at all sure). My java is a little rusty. I believe I mean the Object .equals and .clone rather than assignment, but there's similar semantics involved.Kittle
I just realized I wrote runtime, when I meant compile time!Kittle
According to cppreference on typeid, the typeid of a polymorphic reference describes its dynamic type, so it's at runtime as well. And I'd hope that compilers would be able to eventually simplify typeid comparisons down to a single pointer comparison of the vtable in question, which might be even faster than the pointer indirection and, more importantly, would integrate nicely with inlining the rest of the operator. But all of this is theoretical, I'd simply have to test both.Barnard
I'm sorry for being kind of messy with my replies. I mean the typeid is in runtime, but the first line in my answer is now reading compile time regarding type deduction. Actually, unless you're making thousands of assignments to rings and fields every second, why not take what ever suits you best from a syntactic view?Kittle

© 2022 - 2024 — McMap. All rights reserved.