【问题标题】:C++ interfaces, inheritance, polymorphismC++接口、继承、多态
【发布时间】:2017-09-11 20:00:14
【问题描述】:

想象一下这个场景:

  • 接口I1m1(),由C1类实现
  • 接口I2m2(),由C2类实现
  • 接口I3m3(),由C3类实现

我想定义接受参数arg的函数。

void f1(I1 arg) {
    use m1()
}

void f2([I1, I2] arg) {
    use m1() and m2()
}

void f3([I2, I3] arg) {
    use m2() and m3()
}

那我要定义:

  • 一个“联合”接口I123,包含所有方法,由C123实现
  • 一个“联合”类 C123,它继承了现有类 C1C2C3 的实现方法。

然后我想实例化C123并将其与f1f2f3一起使用。

C123 obj;

f1(obj);
f2(obj);
f3(obj);

问题

  1. 这在 C++ 中可行吗?
  2. 是否可以不使用接口?例如。函数 f2 将采用 [C1, C2] 而不是 [I1, I2]

代码

#include <string>
#include <iostream>

using namespace std;

class C1 {
protected:
  int i;
public:
  int getI() const { return i; }
  void setI(int i_) { i = i_; }
};

class C2 {
protected:
  string s;
public:
  string getS() const { return s; }
  void setS(string s_) { s = s_; }
};

class C3 {
protected:
  float f;
public:
  float getF() const { return f; }
  void setF(float f_) { f = f_; }
};

class C23 : public C2, public C3 {};
class C123 : public C1, public C2, public C3 {};

void f3(C23 arg) {
  arg.setS(to_string(arg.getF()));
}


int main() {

  C123 obj;

  f3(obj);

  std::cout << obj.getS();
}

错误信息

a.cc: In function ‘int main()’:
a.cc:42:9: error: could not convert ‘obj’ from ‘C123’ to ‘C23’
   f3(obj);

【问题讨论】:

  • “一个“联合”接口I123,它具有所有方法,由什么实现?
  • 您可以为从适当接口继承的每个组合创建一个类型(允许多重继承)。当这变得不可行时,使用模板来获得灵活性,但在传递不兼容的类型时会以错误消息清晰为代价。
  • “接口”是什么意思?你有多个抽象类吗?
  • 为什么要把 union 放在 "" 中? union 在 C++ 中具有明确定义的含义,还是您的意思有所不同?如果使用“联合”意味着一个类应该实现所有三个接口的方法,那么只需从所有三个接口继承
  • "没有接口可以吗?"有什么可能?之前的一切都是关于接口的,所以不清楚你想做什么没有

标签: c++ inheritance interface polymorphism


【解决方案1】:
  1. 这在 C++ 中可行吗?

是的,在 C++ 中可以做到这一点。

Example solution using interfaces

#include <string>
#include <iostream>

using namespace std;

class I1 {
public:
    virtual int getI() const = 0;
    virtual void setI(int i) = 0;
};

class I2 {
public:
    virtual string getS() const = 0;
    virtual void setS(string s) = 0;
};

class I3 {
public:
    virtual float getF() const = 0;
    virtual void setF(float f) = 0;
};

class C1 : public I1 {
protected:
    int i;
public:
    int getI() const { return i; }
    void setI(int i_) { i = i_; }
};

class C12 : public I1, public I2 {
protected:
    int i;
    string s;
public:
    int getI() const { return i; }
    void setI(int i_) { i = i_; }
    string getS() const { return s; }
    void setS(string s_) { s = s_; }
};

class C123 : public I1, public I2, public I3 {
protected:
    int i;
    string s;
    float f;
public:
    int getI() const { return i; }
    void setI(int i_) { i = i_; }
    string getS() const { return s; }
    void setS(string s_) { s = s_; }
    float getF() const { return f; }
    void setF(float f_) { f = f_; }
};

template<class T>
void f1(const T& c1)
{
    cout << "f1:\n";
    cout << "  getI: " << c1.getI() << endl;
}

template<class T>
void f2(const T& c12)
{
    cout << "f2:\n";
    cout << "  getI: " << c12.getI() << endl;
    cout << "  getS: " << c12.getS() << endl;
}

template<class T>
void f3(const T& c23)
{
    cout << "f3:\n";
    cout << "  getS: " << c23.getS() << endl;
    cout << "  getF: " << c23.getF() << endl;
}

void test()
{
    C1 c1;
    c1.setI(1);
    f1(c1);

    cout << "\n===== " << endl;

    C12 c12;
    c12.setI(12);
    c12.setS("str12");
    f1(c12);
    f2(c12);

    cout << "\n===== " << endl;

    C123 c123;
    c123.setI(123);
    c123.setF(1.23f);
    c123.setS("str123");
    f1(c123);
    f2(c123);
    f3(c123);

    cout << "\n===== " << endl;
}

int main()
{
    test();
}
  1. 是否可以不使用接口?例如。函数 f2 将采用 [C1, C2] 而不是 [I1, I2]。

是的,没有接口也可以做到这一点。

Solution using virtual inheritance (without using interfaces):

#include <string>
#include <iostream>

using namespace std;

class C1 {
protected:
    int i;
public:
    int getI() const { return i; }
    void setI(int i_) { i = i_; }
};

class C2 {
protected:
    string s;
public:
    string getS() const { return s; }
    void setS(string s_) { s = s_; }
};

class C3 {
protected:
    float f;
public:
    float getF() const { return f; }
    void setF(float f_) { f = f_; }
};

class C12 : public virtual C1, public virtual C2
{
};

class C23 : public virtual C2, public virtual C3
{
};

class C123 : public virtual C1, public virtual C12, public virtual C23
{
};


void f1(const C1& c1)
{
    cout << "f1:\n";
    cout << "  getI: " << c1.getI() << endl;
}

void f2(const C12& c12)
{
    cout << "f2:\n";
    cout << "  getI: " << c12.getI() << endl;
    cout << "  getS: " << c12.getS() << endl;
}

void f3(const C23& c23)
{
    cout << "f3:\n";
    cout << "  getS: " << c23.getS() << endl;
    cout << "  getF: " << c23.getF() << endl;
}

void test()
{
    C1 c1;
    c1.setI(1);
    f1(c1);

    cout << "\n===== " << endl;

    C12 c12;
    c12.setI(12);
    c12.setS("str12");
    f1(c12);
    f2(c12);

    cout << "\n===== " << endl;

    C123 c123;
    c123.setI(123);
    c123.setF(1.23f);
    c123.setS("str123");
    f1(c123);
    f2(c123);
    f3(c123);

    cout << "\n===== " << endl;
}

int main()
{
    test();
}

一些解释。如果您需要 f2 仅采用 C1 和 C2(没有 C3),那么它必须是在 C1 和 C2 上都继承的单独类型。 f3也是一样。然后,如果您遵循该逻辑并在不使用虚拟继承的情况下创建类C12C23,那么现在应该从 C12 和 C23 继承的 C123 将最终得到 C2 的多个副本,并且当您调用 f2 和 f3 时,您会从getS 获取不同的值。虚拟继承确保继承层次结构中只有一个类的副本。

一个“联合”类 C123,它从现有类 C1、C2、C3 继承已实现的方法

Solution that use virtual inheritance and interfaces

#include <string>
#include <iostream>

using namespace std;

class I1 {
public:
    virtual int getI() const = 0;
    virtual void setI(int i) = 0;
};

class I2 {
public:
    virtual string getS() const = 0;
    virtual void setS(string s) = 0;
};

class I3 {
public:
    virtual float getF() const = 0;
    virtual void setF(float f) = 0;
};

class I12 : virtual public I1, virtual public I2 {};
class I23 : virtual public I2, virtual public I3 {};
class I123 : virtual public I12, virtual public I23 {};

class C1 : virtual public I1 {
protected:
    int i;
public:
    int getI() const { return i; }
    void setI(int i_) { i = i_; }
};

class C2 : virtual public I2 {
protected:
    string s;
public:
    string getS() const { return s; }
    void setS(string s_) { s = s_; }
};

class C3 : virtual public I3 {
protected:
    float f;
public:
    float getF() const { return f; }
    void setF(float f_) { f = f_; }
};

class C12 : public I12, public C1, public C2 {};
class C123 : public I123, public C1, public C2, public C3 {};

void f1(const I1& c1)
{
    cout << "f1:\n";
    cout << "  getI: " << c1.getI() << endl;
}

void f2(const I12& c12)
{
    cout << "f2:\n";
    cout << "  getI: " << c12.getI() << endl;
    cout << "  getS: " << c12.getS() << endl;
}

void f3(const I123& c23)
{
    cout << "f3:\n";
    cout << "  getS: " << c23.getS() << endl;
    cout << "  getF: " << c23.getF() << endl;
}

void test()
{
    C1 c1;
    c1.setI(1);
    f1(c1);

    cout << "\n===== " << endl;

    C12 c12;
    c12.setI(12);
    c12.setS("str12");
    f1(c12);
    f2(c12);

    cout << "\n===== " << endl;

    C123 c123;
    c123.setI(123);
    c123.setF(1.23f);
    c123.setS("str123");
    f1(c123);
    f2(c123);
    f3(c123);

    cout << "\n===== " << endl;
}

int main()
{
    test();
}

所有解决方案都应该产生这个输出:

f1:
  getI: 1

=====
f1:
  getI: 12
f2:
  getI: 12
  getS: str12

=====
f1:
  getI: 123
f2:
  getI: 123
  getS: str123
f3:
  getS: str123
  getF: 1.23

=====

【讨论】:

  • 谢谢。这基本上回答了我的第 2 个问题。不知道为什么投反对票。
  • @boofaz 可能有一种方法可以使用虚拟接口制作某些东西,但对于您的特定要求,虚拟继承最适合 IMO。
  • @boofaz 我添加了使用没有虚拟继承的接口的解决方案。
  • 就像 DaveFar 在他的回答中一样,您重新实现了 getIsetI 等方法,而不是继承它们。使用继承的原因是您不必重新实现已经实现的方法。
  • 模板部分是一个解决方案,模板是鸭子类型的,因此您立即支持实现m1()m2() 的任何类型(无论是否使用接口)。您的虚拟继承不起作用,因为现在有一个函数需要使用来自C1C3C123 的成员应该兼容,但事实并非如此。您需要在此处使用鸭子类型来避免基类的组合爆炸,这意味着您需要模板。
【解决方案2】:

是的,这可以通过纯抽象类和多重继承来实现。由于I1I2I3之间没有冲突,所以多重继承不会造成任何问题。

示例实现:

#include <iostream>

class I1 {
public:
    virtual void m1() = 0;
    virtual ~I1(){};
};

class C1 : public I1 {
public:
    void m1() override {
        std::cout << "m1() of a C1" << std::endl;
    }
    ~C1(){};
};

class I2 {
public:
    virtual void m2() = 0;
    virtual ~I2(){};
};

class C2 : public I2 {
public:
    void m2() override {
        std::cout << "m2() of a C2" << std::endl;
    }
    ~C2(){};
};

class I3 {
public:
    virtual void m3() = 0;
    virtual ~I3(){};
};

class C3 : public I3 {
public:
    void m3() override {
        std::cout << "m3() of a C3" << std::endl;
    }
    ~C3(){};
};

class I12 : public I1, public I2 {
public:
    virtual ~I12(){};
};

class I23 : public I2, public I3 {
public:
    virtual ~I23(){};
};

class I123 : public I12, public I23 {
public:
    virtual ~I123(){};
};

class C123 : public I123 {
public:
    void m1() override {
        std::cout << "m1() of a C123" << std::endl;
    }
    void m2() override {
        std::cout << "m2() of a C123" << std::endl;
    }
    void m3() override {
        std::cout << "m3() of a C123" << std::endl;
    }
    ~C123(){};
};

void f1(I1 &arg) {
    std::cout << "f1():" << std::endl;
    arg.m1();
}

void f2(I12 &arg) {
    std::cout << "f2():" << std::endl;
    arg.m1();
    arg.m2();
}

void f3(I23 &arg) {
    std::cout << "f3():" << std::endl;
    arg.m2();
    arg.m3();
}

int main() {
    C123 object;
    f1(object);
    f2(object);
    f3(object);
    return 0;
}

产生以下输出:

f1():
m1() of a C123
f2():
m1() of a C123
m2() of a C123
f3():
m2() of a C123
m3() of a C123

这表明 C++ 提供了满足您要求的多重继承。但是,我建议您阅读Effective C++ Item 40:明智地使用多重继承。本章建议仅在必要时使用虚拟基类。所以当你确实有一个“致命的 MI 钻石”有冲突时,使用上述所有纯虚拟类作为虚拟基类(即所有直接从它继承的类都通过: virtual public .... 进行。代价是性能较低且初始化规则更复杂.


更新:

如果你想继承C123中C1、C2和C3的实现(注意实现继承往往是不好的设计),你可以这样做:

  • 使用I1I2I3I12I23I123 作为虚拟基类,并且
  • 在不重新实现m1()m2()m3()的情况下实现class C123 : virtual public I123, public C1, public C2, public C3

结果输出是

f1():
m1() of a C1
f2():
m1() of a C1
m2() of a C2
f3():
m2() of a C2
m3() of a C3

【讨论】:

  • 那很好。但是I123 被转换为I23 呢?到目前为止,我的所有尝试都没有成功——GCC 说“无法从 C123 转换为 I23”。
  • 我添加了一个实现。
  • I123 包含 2 个不应包含的 I2 副本。
  • 2 个 I2 副本不会导致冲突。每当您遇到冲突时,请使用虚拟基类(或者更好的是,替代多重继承)。
  • 没有虚函数,因为我为了简洁而排除了它们,但I1,I2,I3中当然会有虚函数。如果它没有任何功能,我为什么还要一个接口?
【解决方案3】:

C++ 类型不同于 Java 的类型,因此通过重新评估您的需求来解决问题通常更容易。使用旧行为派生新对象有两种不同的方法,了解每种方法的好处非常重要。

继承是第一个,你基本上复制旧对象的功能,然后添加更多。当您有许多不同的父对象都实现相同的功能时,这将成为一个问题。第二个是composition,您将许多对象放在新对象中。然后新对象可以“选择”使用哪些行为,通常会消除多重继承的歧义。它的代价是您必须定义行为,因为编译器无法对您的预期行为进行如此深入的推理。

在您的情况下,我了解到您有 3 个行为不同的对象,您需要组合(而不是继承)它们的行为。您描述您需要为组合对象中包含的任何I# 调用m();这很容易。

定义一个包装对象,有条件地调用各个接口提供的m函数:

class Wrapper {
    I1 slot1;
    I2 slot2;
    I3 slot3;

    Wrapper(i1 = I1.default(), i2 = I2.default(), i3 = I3.default()) {
        slot1 = i1;
        slot2 = i2;
        slot3 = i3;
    }

    void foo() {
        // The default instances don't do anything on m()
        slot1::m();
        slot2::m();
        slot3::m();
    }

}

无疑还有很大的改进和优化空间,但这是大体思路。

查看您的示例代码,您可能希望查看Logger 的概念以进行调试。许多语言都使用它们,您“订阅”cout/cerr 到程序不同部分提供的不同记录器提要。例如,您可能有一个物理记录器和另一个图形记录器。

它们看起来非常类似于您选择性地输出对象中可用的不同数据类型。

【讨论】:

  • 默认值太糟糕了,你应该感觉很糟糕,你也忘了用::而不是.
  • @Puppy 是的,绝对不理想。我不是模板向导,所以我主要关注事物的概念方面。感谢您发现我的错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-30
  • 2013-11-25
  • 2022-08-12
  • 2010-11-10
  • 2011-03-24
  • 1970-01-01
  • 2023-03-14
相关资源
最近更新 更多