Method chaining + inheritance don't play well together?

后端 未结 15 766
北荒
北荒 2020-12-08 22:20

Consider:

// member data omitted for brevity

// assume that \"setAngle\" needs to be implemented separately
// in Label and Image, and that Button does need         


        
相关标签:
15条回答
  • 2020-12-08 22:57

    Do setText() and setAngle() really need to return their own types in each class? If you set them all to return Widget&, then you can just use virtual functions as follows:

    struct Widget {
        Widget& move(Point newPos) { pos = newPos; return *this; }
        virtual Widget& setText(string const& newText) = 0;
        virtual Widget& setAngle(double newAngle) = 0;
    };
    
    struct Label : Widget {
        virtual Widget& setText(string const& newText) { text = newText; return *this; }
        virtual Widget& setAngle(double newAngle) { angle = newAngle; return *this; }
    };
    
    struct Button : Label {
        virtual Widget& setAngle(double newAngle) {
            backgroundImage.setAngle(newAngle);
            Label::setAngle(newAngle);
            return *this;
        }
    };
    

    Note that even if the return type is Widget&, the Button- or Label-level functions will still be the ones called.

    0 讨论(0)
  • 2020-12-08 22:58

    Not with C++.

    C++ does not support variance in return types, so there's no way to change the static type of the reference returned from Widget.move() to be more specific than Widget& even if you override it.

    The C++ needs to be able to check things at compile time, so you can't use the fact that what's really being returned from move is a button.

    At best, you can do some runtime casting, but it's not going to look pretty. Just separate calls.

    Edit: Yes, I'm well aware of the fact that the C++ standard says that return value covariance is legitimate. However, at the time I was teaching and practicing C++, some mainstream compilers (e.g., VC++) did not. Hence, for portability we recommended against it. It is possible that current compilers have no issue with that, finally.

    0 讨论(0)
  • 2020-12-08 23:01

    Other people have hit on design issues. You can sort of workaround the problem (albeit in a pretty gross fashion) using C++'s support for covariant return types.

    struct Widget {
        virtual Widget& move(Point newPos) { pos = newPos; return *this; }
    };
    
    struct Label : Widget {
        Label& move(Point newPos) { pos = newPos; return *this; }
        Label& setText(string const& newText) { text = newText; return *this; }
        Label& setAngle(double newAngle) { angle = newAngle; return *this; }
    };
    
    struct Button : Label {
        Button& setAngle(double newAngle) {
            backgroundImage.setAngle(newAngle);
            Label::setAngle(newAngle);
            return *this;
        }
    };
    
    int main() {
        Button btn;
    
        // oops: Widget::setText doesn't exist
        btn.move(Point(0,0)).setText("Hey");
    
        // oops: calling Label::setAngle rather than Button::setAngle
        btn.setText("Boo").setAngle(.5); 
    }
    

    Really though chaining methods the way you are makes for strange code, and hurts readability rather than helps. If you killed the return reference to self junk your code would become:

    struct Widget {
        void move(Point newPos) { pos = newPos; }
    };
    
    struct Label : Widget {
        void setText(string const& newText) { text = newText; }
        void setAngle(double newAngle) { angle = newAngle; }
    };
    
    struct Button : Label {
        void setAngle(double newAngle) {
            backgroundImage.setAngle(newAngle);
            Label::setAngle(newAngle);
        }
    };
    
    int main() {
        Button btn;
    
        // oops: Widget::setText doesn't exist
        btn.move(Point(0,0));
        btn.setText("Hey");
    
        // oops: calling Label::setAngle rather than Button::setAngle
        btn.setText("Boo");
        btn.setAngle(.5); 
    }
    
    0 讨论(0)
  • 2020-12-08 23:05

    For the second problem, making setAngle virtual should do the trick.

    For the first one, there are no easy solutions. Widget::move returns a Widget, which doesn't have a setText method. You could make a pure virtual setText method, but that'd be a pretty ugly solution. You could overload move() on the button class, but that'd be a pain to maintain. Finally, you could probably do something with templates. Perhaps something like this:

    // Define a move helper function
    template <typename T>
    T& move(T& obj, Point& p){ return obj.move(p); };
    
    // And the problematic line in your code would then look like this:
    move(btn, Point(0,0)).setText("Hey");
    

    I'll let you decide which solution is cleanest. But is there any particular reason why you need to be able to chain these methods?

    0 讨论(0)
  • 2020-12-08 23:05

    Really, method chaining is a bad idea due to poor exception safety. Do you really need it?

    0 讨论(0)
  • 2020-12-08 23:06

    For a while there I thought that it might be possible to overload the slightly unusual operator->() for chaining methods instead of ., but that faltered because it seems the compiler requires the identifier to the right of the -> to belong to the static type of the expression on the left. Fair enough.

    Poor Man's Method Chaining

    Stepping back for a moment, the point of method chaining is to avoid typing out long object names repeatedly. I'll suggest the following quick and dirty approach:

    Instead of the "longhand form":

    btn.move(Point(0,0)); btn.setText("Hey");
    

    You can write:

    {Button& _=btn; _.move(Point(0,0)); _.setText("Hey");}
    

    No, it's not as succinct as real chaining with ., but it will save some typing when there are many parameters to set, and it does have the benefit that it requires no code changes to your existing classes. Because you wrap the entire group of method calls in {} to limit the scope of the reference, you can always use the same short identifier (e.g. _ or x) to stand for the particular object name, potentially increasing readability. Finally, the compiler will have no trouble optimising away _.

    0 讨论(0)
提交回复
热议问题