C++中接口类和实现类的并行继承

发布于 2024-10-02 16:10:44 字数 1383 浏览 3 评论 0原文

我正在尝试以与 Java 接口类似的方式使用 C++ 抽象基类。假设我们有以下仅具有纯虚函数的接口类:

class Shape { virtual double area()=0; };
class Square : public Shape { virtual void setLength(double length)=0; };
class Rectangle : public Square { virtual void setWidth(double width)=0; };

并且我尝试按以下方式实现 Square 和 Rectangle:

class SquareImpl : public Square { /*implementation*/ };
class RectangleImpl : public SquareImpl, Rectangle { /*implementation*/ };

其中 RectangleImpl 继承 SquareImplRectangle< /code> 重用,例如 SquareImpl::area()。然而,当我尝试编译时,出现了两个问题:首先, SquareImpl 中的所有方法都没有正确继承,我必须手动重新实现 RectangleImpl::area() 和 <代码> RectangleImpl::setLength() 。其次,这仍然引入了菱形问题,即 ShapeRectangleImpl 的基数不明确。

如果我实际上从 Shape 继承 Square,我可以编译代码,但我认为性能不会随着添加更多派生接口而扩展。同样奇怪的是,尽管 SquareImpl::area() 继承得很好,但 RectangleImpl 仍然没有继承 SquareImpl::setLength() 。 (这里忽略实用性)

另一种解决方案可能是使接口彼此独立,即使Square不继承自Shape。但如果我定义采用 Square* 指针的函数,这样做将使我无法访问 Shape 中的方法。它还将使 ShapeSquare 之间无法进行 static_cast。

所以我的问题是,C++中是否有其他设计模式可以解决这种接口类和实现类之间的并行继承,而不需要虚拟继承?

(编辑说明:上面的示例代码只是我关于接口和实现之间并行继承的虚拟说明。我知道有更好的方法来实现形状,但我的问题不在于如何实现形状。)

I'm trying to use C++ abstract base class in the way similar with Java interface. Supposed that we have following interface classes with only pure virtual functions:

class Shape { virtual double area()=0; };
class Square : public Shape { virtual void setLength(double length)=0; };
class Rectangle : public Square { virtual void setWidth(double width)=0; };

and I try to implement Square and Rectangle the following way:

class SquareImpl : public Square { /*implementation*/ };
class RectangleImpl : public SquareImpl, Rectangle { /*implementation*/ };

Where RectangleImpl inherits both SquareImpl and Rectangle to reuse, say, SquareImpl::area(). However when I try to compile, two problems arise: Firstly, all methods in SquareImpl do not get inherited properly and I have to manually reimplement RectangleImpl::area() and RectangleImpl::setLength(). Secondly, this still introduces the diamond problem that Shape is ambiguous base of RectangleImpl.

I could compile the code if I virtually inherit Square from Shape, but I don't think the performance will scale with more derived interfaces added. Also strangely, RectangleImpl still doesn't inherit SquareImpl::setLength() although SquareImpl::area() is inherited well. (ignore the practicality here)

Another solution might be to make interfaces independent of each other, i.e. to make Square not inherited from Shape. But doing so will make me lose access to methods in Shape if I define functions that take a Square* pointer. It will also make static_cast impossible between Shape and Square.

So my question is, is there any other design pattern in C++ to solve this kind of parallel inheritance between interface classes and implementation classes, without requiring virtual inheritance?

(edit clarification: the example code above are just my dummy illustration on parallel inheritance between interfaces and implementations. I understand that there are better ways to implement shapes but my problem is not on how to implement shapes.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

拥抱影子 2024-10-09 16:10:44

这里的情况是钻石问题,它可能发生在任何允许的面向对象语言中多重继承。顺便说一下,这就是 Java 设计者决定不使用多重继承并提出接口概念的原因之一。

C++ 处理钻石问题的方式是虚拟继承

而且,正如 codymanix 指出的那样,正方形和矩形对于面向对象设计来说是众所周知的糟糕示例,因为就 OO 而言 正方形不是矩形

再加上几点。首先,您在这里所做的术语是多重继承,而不是“并行继承”。其次,在这种特殊情况下,拥有一个 Square 类和 SquareImpl 类确实没有什么意义。如果您认为您可能有不同的 Square 实现,那么您应该只拥有一个基类,该基类提供默认实现和虚拟函数,必要时可以由派生类重写这些函数。换句话说,您应该将 SquareSquareImpl 合并到一个具有虚函数的类中。

您当然可以像 Java 接口一样使用抽象 C++ 类,但大多数时候没有理由这样做。 Java 中添加接口正是为了解决缺乏多重继承的问题。在 C++ 中,您可以继续使用多重继承,尽管您应该始终非常明智地这样做。

What you have here is the case of the Diamond Problem, which can happen in any OO language that allows multiple inheritance. This, by the way, is one reason why the designers of Java decided not to have multiple inheritance, and came up with the notion of an interface.

The way C++ deals with the diamond problem is Virtual Inheritance.

And, as codymanix pointed out, the square and the rectangle is a notoriously bad example for object oriented design, because as far as OO is concerned a square is not a rectangle.

Couple more points. First, The term for what you are doing here is multiple inheritance, not "parallel inheritance". Second, in this particular case it really makes little sense to have a class Square and a class SquareImpl. If you think you may have different implementations of Square, you should just have one base class which provides a default implementation and virtual functions that can be overridden by a derived class if necessary. In other words, you should roll Square and SquareImpl into one class with virtual functions.

You certainly can use an abstract C++ class like a Java interface, but most of the time there is no reason for it. Interfaces were added to Java precisely as a way to get around the lack of multiple inheritance. In C++ you can just go ahead and use multiple inheritance, although you should always do that very judiciously.

也只是曾经 2024-10-09 16:10:44

到目前为止,您并不是第一个遇到此问题的人。请参阅正方形不是矩形给出一个例子。

You are by far not the first who met this problem. See A Square Is Not a Rectangle to give one example.

难忘№最初的完美 2024-10-09 16:10:44

经过一晚上的重新思考并参考肖恩提供的解决方案 在C++中寻找比虚拟继承更好的方法,我提出了以下解决方案。

在这里我将问题重新定义得更加抽象,以避免我们对形状产生混淆。我们有一个可以滚动的 Ball 接口,一个包含 Foo 特定方法的 FooBall 接口,以及一个也是 FooBarBall 接口FooBall 并包含 Foo 特定和 Bar 特定方法。与原始问题相同,我们有一个 FooBall 实现,并且我们希望派生它来覆盖 Bar 特定的方法。但同时继承接口和实现将引入钻石继承。

为了解决这个问题,我没有直接将 Foo 和 Bar 特定方法放入派生的 Ball 接口中,而是将单个方法放入派生的 FooBall 接口中,该接口将对象转换为通过 toFoo() 方法获取 Foo 对象。这样,实现就可以混合独立的 FooBar 接口,而无需引入菱形继承。

尽管如此,并不是所有的代码都可以被消除以从 Foos 中自由地派生出所有的 Bar。我们仍然必须编写 BallFooBallFooBarBall 的独立实现,它们不能互相继承。但是我们可以使用复合模式来包装以不同方式实现的真实 FooBar 对象。这样,如果我们有很多 Foo 和 Bar 的实现,我们仍然可以消除相当多的代码。

#include <stdio.h>

class Ball {
  public:
    // All balls can roll.
    virtual void roll() = 0;

    // Ball has many other methods that are not
    // covered here.

    virtual inline ~Ball() {
        printf("deleting Ball\n");
    };
};

class Foo {
  public:
    virtual void doFoo() = 0;

    // do some very complicated stuff.
    virtual void complexFoo() = 0;

    virtual inline ~Foo() {};
};

/** 
 * We assume that classes that implement Bar also
 * implement the Foo interface. The Bar interface
 * specification failed to enforce this constraint
 * by inheriting from Foo because it will introduce
 * diamond inheritance into the implementation.
 **/
class Bar {
  public:
    virtual void doBar() = 0;
    virtual void complicatedBar() = 0;

    virtual inline ~Bar() {};
};

class FooBall : public Ball {
  public:
    virtual Foo* toFoo() = 0;

    virtual inline ~FooBall() {};
};

/**
 * A BarBall is always also a FooBall and support
 * both Foo and Bar methods.
 **/
class FooBarBall : public FooBall {
  public:
    virtual Bar* toBar() = 0;

    virtual inline ~FooBarBall() {};
};


/* Composite Implementation */

class FooImpl_A : public Foo {
  public:
    virtual void doFoo() {
        printf("FooImpl_A::doFoo()\n");
    };

    virtual void complexFoo() {
        printf("FooImpl_A::complexFoo()\n");
    }

    virtual inline ~FooImpl_A() {
        printf("deleting FooImpl_A\n");
    }
};

class FooBarImpl_A : public FooImpl_A, public Bar {
  public:
    virtual void doBar() {
        printf("BarImpl_A::doBar()\n");
    }

    virtual void complicatedBar() {;
        printf("BarImpl_A::complicatedBar()\n");
    }

    virtual inline ~FooBarImpl_A() {
        printf("deleting FooBarImpl_A\n");
    }
};

/* Composite Pattern */
class FooBarBallContainer : public FooBarBall {
  public:

    /* FooBarBallImpl_A can take any class that
     * implements both the Foo and Bar interface, 
     * including classes that inherit FooBarImpl_A
     * and other different implementations.
     *
     * We'll assume that realFoo and realBar are
     * actually the same object as Foo methods have
     * side effect on Bar methods. If they are not
     * the same object, a third argument with false
     * value need to be supplied.
     */
    FooBarBallContainer( Foo* realFoo, Bar* realBar, bool sameObject=true ) : 
    _realFoo(realFoo), _realBar(realBar), _sameObject(sameObject) {}

    virtual void roll() {
        // roll makes use of FooBar methods
        _realBar->doBar();
        _realFoo->complexFoo();
    }

    virtual Foo* toFoo() {
        return _realFoo;
    }

    virtual Bar* toBar() {
        return _realBar;
    }

    virtual ~FooBarBallContainer() {
        delete _realFoo;

        // Check if realFoo and realBar are
        // not the same object to avoid deleting
        // it twice.
        if(!_sameObject) {
            delete _realBar;
        }
    }

  private:
    Foo* _realFoo;
    Bar* _realBar;
    bool _sameObject;
};


/* Monolithic Implmentation */

class FooBarBallImpl_B : public FooBarBall,
    public Foo, public Bar {

  public:
    virtual void roll() {
        complicatedBar();
        doFoo();
    }

    virtual Foo* toFoo() {
        return (Foo*) this;
    }

    virtual Bar* toBar() {
        return (Bar*) this;
    }

    virtual void doFoo() {
        printf("FooBarBallImpl_B::doFoo()\n");
    }

    virtual void complexFoo() {
        printf("FooBarBallImpl_B::complexFoo()\n");
    }

    virtual void doBar() {
        printf("FooBarBallImpl_B::doBar()\n");
    }

    virtual void complicatedBar() {
        printf("FooBarBallImpl_B::complicatedBar()\n");
    }

};

/* Example usage of FooBarBall */
void processFooBarBall(FooBarBall *ball) {

    Foo *foo = ball->toFoo();
    foo->doFoo();

    ball->roll();

    Bar *bar = ball->toBar();
    bar->complicatedBar();
}

main() {

    FooBarImpl_A *fooBar = new FooBarImpl_A();
    FooBarBall *container = new FooBarBallContainer(fooBar, fooBar);

    printf
    processFooBarBall(container);
    delete container;

    FooBarBallImpl_B *ball = new FooBarBallImpl_B();
    processFooBarBall(ball);

    // we can even wrap FooBarBallImpl_B into the container
    // but the behavior of roll() will become different
    container = new FooBarBallContainer(ball, ball);
    processFooBarBall(container);

    delete container;

}

After rethinking for a night and referring to the solution sean provided at Looking for a better way than virtual inheritance in C++, I came out with the following solution.

Here I redefine the problem to be more abstract to avoid the confusion we had on shapes. We have a Ball interface that can roll, a FooBall interface that contains Foo specific methods, and a FooBarBall interface that is also a FooBall and contains both Foo specific and Bar specific methods. Same as the original problem, we have a FooBall implementation and we wish to derive it to cover Bar specific methods as well. but inheriting both the interface and implementation will introduce diamond inheritance.

To solve the problem, instead of directly putting Foo and Bar specific methods into the derived Ball interfaces, I put a single method into a derived FooBall interface that converts the object into a Foo object through the toFoo() method. This way, implementations can mix in the independent Foo and Bar interface without introducing diamond inheritance.

Still, not all codes can be eliminated to derive all Bars from Foos freely. We have to still write independent implementations of Ball, FooBall, and FooBarBall that do not inherit from each others. But we can use the composite pattern to wrap the real Foo and Bar objects that are implemented differently. This way we can still eliminate quite a lot of code if we have a lot of implementations of Foo and Bar.

#include <stdio.h>

class Ball {
  public:
    // All balls can roll.
    virtual void roll() = 0;

    // Ball has many other methods that are not
    // covered here.

    virtual inline ~Ball() {
        printf("deleting Ball\n");
    };
};

class Foo {
  public:
    virtual void doFoo() = 0;

    // do some very complicated stuff.
    virtual void complexFoo() = 0;

    virtual inline ~Foo() {};
};

/** 
 * We assume that classes that implement Bar also
 * implement the Foo interface. The Bar interface
 * specification failed to enforce this constraint
 * by inheriting from Foo because it will introduce
 * diamond inheritance into the implementation.
 **/
class Bar {
  public:
    virtual void doBar() = 0;
    virtual void complicatedBar() = 0;

    virtual inline ~Bar() {};
};

class FooBall : public Ball {
  public:
    virtual Foo* toFoo() = 0;

    virtual inline ~FooBall() {};
};

/**
 * A BarBall is always also a FooBall and support
 * both Foo and Bar methods.
 **/
class FooBarBall : public FooBall {
  public:
    virtual Bar* toBar() = 0;

    virtual inline ~FooBarBall() {};
};


/* Composite Implementation */

class FooImpl_A : public Foo {
  public:
    virtual void doFoo() {
        printf("FooImpl_A::doFoo()\n");
    };

    virtual void complexFoo() {
        printf("FooImpl_A::complexFoo()\n");
    }

    virtual inline ~FooImpl_A() {
        printf("deleting FooImpl_A\n");
    }
};

class FooBarImpl_A : public FooImpl_A, public Bar {
  public:
    virtual void doBar() {
        printf("BarImpl_A::doBar()\n");
    }

    virtual void complicatedBar() {;
        printf("BarImpl_A::complicatedBar()\n");
    }

    virtual inline ~FooBarImpl_A() {
        printf("deleting FooBarImpl_A\n");
    }
};

/* Composite Pattern */
class FooBarBallContainer : public FooBarBall {
  public:

    /* FooBarBallImpl_A can take any class that
     * implements both the Foo and Bar interface, 
     * including classes that inherit FooBarImpl_A
     * and other different implementations.
     *
     * We'll assume that realFoo and realBar are
     * actually the same object as Foo methods have
     * side effect on Bar methods. If they are not
     * the same object, a third argument with false
     * value need to be supplied.
     */
    FooBarBallContainer( Foo* realFoo, Bar* realBar, bool sameObject=true ) : 
    _realFoo(realFoo), _realBar(realBar), _sameObject(sameObject) {}

    virtual void roll() {
        // roll makes use of FooBar methods
        _realBar->doBar();
        _realFoo->complexFoo();
    }

    virtual Foo* toFoo() {
        return _realFoo;
    }

    virtual Bar* toBar() {
        return _realBar;
    }

    virtual ~FooBarBallContainer() {
        delete _realFoo;

        // Check if realFoo and realBar are
        // not the same object to avoid deleting
        // it twice.
        if(!_sameObject) {
            delete _realBar;
        }
    }

  private:
    Foo* _realFoo;
    Bar* _realBar;
    bool _sameObject;
};


/* Monolithic Implmentation */

class FooBarBallImpl_B : public FooBarBall,
    public Foo, public Bar {

  public:
    virtual void roll() {
        complicatedBar();
        doFoo();
    }

    virtual Foo* toFoo() {
        return (Foo*) this;
    }

    virtual Bar* toBar() {
        return (Bar*) this;
    }

    virtual void doFoo() {
        printf("FooBarBallImpl_B::doFoo()\n");
    }

    virtual void complexFoo() {
        printf("FooBarBallImpl_B::complexFoo()\n");
    }

    virtual void doBar() {
        printf("FooBarBallImpl_B::doBar()\n");
    }

    virtual void complicatedBar() {
        printf("FooBarBallImpl_B::complicatedBar()\n");
    }

};

/* Example usage of FooBarBall */
void processFooBarBall(FooBarBall *ball) {

    Foo *foo = ball->toFoo();
    foo->doFoo();

    ball->roll();

    Bar *bar = ball->toBar();
    bar->complicatedBar();
}

main() {

    FooBarImpl_A *fooBar = new FooBarImpl_A();
    FooBarBall *container = new FooBarBallContainer(fooBar, fooBar);

    printf
    processFooBarBall(container);
    delete container;

    FooBarBallImpl_B *ball = new FooBarBallImpl_B();
    processFooBarBall(ball);

    // we can even wrap FooBarBallImpl_B into the container
    // but the behavior of roll() will become different
    container = new FooBarBallContainer(ball, ball);
    processFooBarBall(container);

    delete container;

}
信愁 2024-10-09 16:10:44

正方形不是矩形,矩形也不是正方形。它们唯一的共同点是它们都是形状。所以:

class Square : public Shape {...};
class Rectangle : public Shape {...};

它们的初始化函数是不同的,Square::setSide(double)Rectangle::setLengthAndWidth(double, double)。您不需要 *Impl 类。在正方形和长方形中做你的事情。

Square is not Rectangle, and Rectangle is not Square. Only thing they have in common is that they are Shapes. So:

class Square : public Shape {...};
class Rectangle : public Shape {...};

Their initialization functions are different, Square::setSide(double) and Rectangle::setLengthAndWidth(double, double). You don't need *Impl classes. Do your stuff in in Square and Rectangle.

吾家有女初长成 2024-10-09 16:10:44

我认为您应该在这里寻找虚拟继承,以便您在其之下只有一个形状实例 - 从语义的角度来看,这似乎是显而易见的 - 即 square 和矩形impl 的形状显然是相同的形状。

不太确定你提到的性能问题 - 这对我来说似乎不是问题(从某种意义上说,除了调用任何 v 函数之外,它们没有额外的开销)。
我不明白为什么你不能从 square 访问 setLength - 很难理解为什么你可能会遇到这种情况并且你没有实现的源代码。

I think you should be looking for virtual inheritance here so that you only have a single instance of shape underneath it all - this seems obvious from a semantic point of view - i.e. The shape that square and rectangleimpl is clearly the same shape.

Not really sure of the performance issue you mention - that doesn't seem an issue to me (in the sense that their is no extra overhead other than that of calling any v-function).
I don't see why you can't access the setLength from square - difficult to see why you might be experiencing this and you don;t have source for the implementations.

吾家有女初长成 2024-10-09 16:10:44

您的问题是 Rectangle->Square->Shape 对 SquareImpl 一无所知,因此它无法使用这些函数来满足其抽象函数要求。

最简单的方法是当接口和实现继承像这样紧密绑定时不要混合它们。使 RectangleImpl 继承自 Square 和 Rectangle 接口。如果 SquareImpl 和 RectangleImpl 相互复制太多代码,请使用一个类来完成所有这些工作,并在每个实现中将其作为成员函数。

Your problem is Rectangle->Square->Shape knows nothing about the SquareImpl so it cannot use these functions to satisfy its abstract function requirements.

The easiest way is to not mix your interface and implementation inheritance when they are closely bound like this. Make RectangleImpl inherit from the Square and Rectangle interfaces. If SquareImpl and RectangleImpl are replicating too much of each other's code, use a class that does all this work and have as a member function in each implementation.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文