ChatGPT解决这个技术问题 Extra ChatGPT

如何在 C++ 中声明接口?

如何设置代表接口的类?这只是一个抽象基类吗?


C
Community

要扩展 bradtgmurray 的答案,您可能希望通过添加虚拟析构函数来对接口的纯虚拟方法列表进行一个例外处理。这允许您在不暴露具体派生类的情况下将指针所有权传递给另一方。析构函数不需要做任何事情,因为接口没有任何具体的成员。将函数定义为虚拟函数和内联函数似乎是矛盾的,但请相信我——事实并非如此。

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

您不必包含虚拟析构函数的主体 - 事实证明,某些编译器在优化空析构函数时遇到了麻烦,您最好使用默认值。


虚拟描述符++!这个非常重要。您可能还希望包含 operator= 的纯虚拟声明并复制构造函数定义,以防止编译器为您自动生成这些声明。
虚拟析构函数的替代方法是受保护的析构函数。这会禁用多态破坏,这在某些情况下可能更合适。在 gotw.ca/publications/mill18.htm 中查找“指南 #4”。
另一种选择是使用主体定义纯虚拟 (=0) 析构函数。这里的优点是编译器理论上可以看到 vtable 现在没有有效成员,并完全丢弃它。使用带有主体的虚拟析构函数,可以(虚拟)调用所述析构函数,例如在构造中间通过 this 指针(当构造的对象仍然是 Parent 类型时),因此编译器必须提供有效的 vtable .因此,如果您在构造过程中没有通过 this 显式调用虚拟析构函数 :) 您可以节省代码大小。
最典型的 C++ 答案没有直接回答问题(尽管显然代码是完美的),而是优化了简单的答案。
不要忘记,在 C++11 中,您可以指定 override 关键字以允许编译时参数和返回值类型检查。例如,在 Child virtual void OverrideMe() override; 的声明中
M
Maik Lowrey

使用纯虚方法创建一个类。通过创建另一个覆盖这些虚拟方法的类来使用该接口。

纯虚方法是定义为虚并赋值为 0 的类方法。

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            // do stuff
        }
};

你应该在 IDemo 中有一个什么都不做的析构函数,以便定义要执行的行为: IDemo *p = new Child; /*随便*/删除p;
为什么 Child 类中的 OverrideMe 方法是虚拟的?那有必要吗?
@Cemre - 不,没有必要,但也没有伤害。
每当覆盖虚拟方法时,保留关键字“virtual”通常是一个好主意。虽然不是必需的,但它可以使代码更清晰 - 否则,您没有迹象表明该方法可以多态使用,甚至存在于基类中。
@Kevin 除了 C++11 中的 override
R
Raedwald

除了 C#/Java 中的抽象基类之外,您还有一个特殊的接口类型类别的全部原因是因为 C#/Java 不支持多重继承。

C++ 支持多重继承,因此不需要特殊类型。没有非抽象(纯虚拟)方法的抽象基类在功能上等同于 C#/Java 接口。


能够创建接口仍然很好,让我们免于输入这么多(virtual,=0,virtual destructor)。此外,多重继承对我来说似乎是一个非常糟糕的主意,我从未见过它在实践中使用过,但一直需要接口。糟糕的是,C++ 礼遇不会仅仅因为我想要它们就引入接口。
Ha11owed:它有接口。它们被称为具有纯虚拟方法且没有方法实现的类。
@doc:java.lang.Thread 具有您可能不想在对象中包含的方法和常量。如果您从 Thread 和另一个具有公共方法 checkAccess() 的类扩展,编译器应该怎么做?你真的更喜欢像 C++ 中那样使用强命名的基指针吗?这似乎是糟糕的设计,您通常需要在您认为需要多重继承的地方进行组合。
@Ha11owed 那是很久以前的事了,所以我不记得细节了,但它有我想在我的类中拥有的方法和内容,更重要的是我希望我的派生类对象是一个 Thread 实例。多重继承可能是糟糕的设计和组合。这一切都取决于情况。
@戴夫:真的吗? Objective-C 有编译时评估和模板?
M
Morgoth

C++ 中没有“接口”的概念。 AFAIK,接口最初是在 Java 中引入的,以解决缺乏多重继承的问题。事实证明,这个概念非常有用,在 C++ 中使用抽象基类也可以达到相同的效果。

抽象基类是一个类,其中至少一个成员函数(Java 术语中的方法)是使用以下语法声明的纯虚函数:

class A
{
  virtual void foo() = 0;
};

不能实例化抽象基类,即不能声明类 A 的对象。只能从 A 派生类,但任何不提供 foo() 实现的派生类也将是抽象类。为了停止抽象,派生类必须为其继承的所有纯虚函数提供实现。

请注意,抽象基类可以不仅仅是一个接口,因为它可以包含非纯虚拟的数据成员和成员函数。接口的等价物将是一个抽象基类,没有任何数据,只有纯虚函数。

而且,正如 Mark Ransom 指出的那样,抽象基类应该提供一个虚拟析构函数,就像任何基类一样。


我想说的不仅仅是“缺乏多重继承”,而是取代多重继承。 Java 从一开始就是这样设计的,因为多重继承带来的问题多于它解决的问题。好答案
Oscar,这取决于您是学习 Java 的 C++ 程序员,反之亦然。 :) 恕我直言,如果使用得当,就像 C++ 中的几乎任何东西一样,多重继承可以解决问题。 “接口”抽象基类是一个非常明智地使用多重继承的例子。
@OscarRyz 错了。 MI 只有在误用时才会产生问题。大多数所谓的 MI 问题也会提出替代设计(没有 MI)。当人们对 MI 的设计有问题时,那是 MI 的错;如果他们对 SI 有设计问题,那是他们自己的错。 “死亡钻石”(重复继承)就是一个典型的例子。 MI 抨击不是纯粹的虚伪,而是接近。
从语义上讲,接口不同于抽象类,因此 Java 的接口不仅仅是一种技术变通方法。定义接口或抽象类之间的选择是由语义驱动的,而不是技术考虑。让我们想象一些接口“HasEngine”:这是一个方面,一个特性,它可以应用于/实现非常不同的类型(无论是类还是抽象类),所以我们将为此定义一个接口,而不是抽象类。
@MarekStanley,您可能是对的,但我希望您选择了一个更好的例子。我喜欢从继承接口与继承实现的角度来考虑它。在 C++ 中,您可以同时继承接口和实现(公共继承),也可以只继承实现(私有继承)。在 Java 中,您可以选择仅继承接口,而无需实现。
B
Boris Dalstein

据我测试,添加虚拟析构函数非常重要。我正在使用用 new 创建并用 delete 销毁的对象。

如果接口中不添加虚析构函数,那么继承类的析构函数就不会被调用。

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

如果您在没有 virtual ~IBase() {}; 的情况下运行前面的代码,您将看到析构函数 Tester::~Tester() 从未被调用。


此页面上的最佳答案,因为它通过提供一个实用的、可编译的示例来说明这一点。干杯!
Testet::~Tester() 仅在 obj 为“Declared with Tester”时运行。
实际上,字符串 privatename 的析构函数将被调用,并且在内存中,这就是所有将被分配的内容。就运行时而言,当类的所有具体成员都被销毁时,类实例也会被销毁。我对具有两个 Point 结构的 Line 类进行了类似的实验,发现两个结构都在删除调用或从包含函数返回时被破坏(哈!)。 valgrind 确认 0 泄漏。
J
Jason Plank

我的回答与其他人基本相同,但我认为还有两件重要的事情要做:

如果有人试图删除 IDemo 类型的对象,请在您的接口中声明一个虚拟析构函数或创建一个受保护的非虚拟析构函数以避免未定义的行为。使用虚拟继承来避免多重继承的问题。 (当我们使用接口时,更常见的是多重继承。)

和其他答案一样:

使用纯虚方法创建一个类。

通过创建另一个覆盖这些虚拟方法的类来使用该接口。类 IDemo { public: virtual void OverrideMe() = 0; virtual ~IDemo() {} } 或 class IDemo { public: virtual void OverrideMe() = 0; protected: ~IDemo() {} } And class Child : virtual public IDemo { public: virtual void OverrideMe() { //do stuff } }


不需要虚拟继承,因为接口中没有任何数据成员。
虚拟继承对于方法也很重要。没有它,您将在使用 OverrideMe() 时遇到歧义,即使它的“实例”之一是纯虚拟的(我自己也试过了)。
@Avishay_ “不需要虚拟继承,因为接口中没有任何数据成员。”错误的。
请注意,虚拟继承可能不适用于某些 gcc 版本,如 WinAVR 2010 附带的版本 4.3.3:gcc.gnu.org/bugzilla/show_bug.cgi?id=35067
-1 有一个非虚拟保护的析构函数,对不起
C
Community

在 C++11 中,您可以轻松地完全避免继承:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

在这种情况下,接口具有引用语义,即您必须确保对象的寿命比接口长(也可以使用值语义制作接口)。

这些类型的接口各有利弊:

它们比基于继承的多态性需要更多的内存。

它们通常比基于继承的多态性更快。

在您知道最终类型的情况下,它们要快得多! (一些编译器如 gcc 和 clang 对不具有/继承自具有虚函数的类型的类型执行更多优化)。

最后,继承是复杂软件设计中万恶之源。在 Sean Parent's Value Semantics and Concepts-based Polymorphism 中(强烈推荐,该技术的更好版本在那里解释)研究了以下案例:

假设我有一个应用程序,我在其中使用 MyShape 接口以多态方式处理我的形状:

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

在您的应用程序中,您可以使用 YourShape 界面对不同的形状执行相同的操作:

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

现在假设您想使用我在您的应用程序中开发的一些形状。从概念上讲,我们的形状具有相同的界面,但要使我的形状在您的应用程序中工作,您需要按如下方式扩展我的形状:

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

首先,修改我的形状可能根本不可能。此外,多重继承导致了意大利面条代码(想象第三个项目使用 TheirShape 接口......如果他们也调用他们的绘图函数 my_draw 会发生什么?)。

更新:有一些关于基于非继承的多态性的新参考:

Sean Parent's Inheritance 是恶语的基类。

Sean Parent 的价值语义和基于概念的多态性演讲。

Pyry Jahkola 的无继承多态性演讲和 poly 库文档。

Zach Laine 的实用类型擦除:用优雅的设计模式演讲解决 OOP 问题。

Andrzej 的 C++ 博客 - 类型擦除部分 i、ii、iii 和 iv。

运行时多态泛型编程——在 ConceptC++ 中混合对象和概念

Boost.TypeErasure 文档

Adobe Poly 文档

Boost.Any,std::any 提案(修订版 3),Boost.Spirit::hold_any。


TBH 继承远比 C++11 的东西清晰得多,它伪装成一个接口,而是绑定一些不一致的设计的粘合剂。 Shapes 示例与现实脱节,Circle 类设计不佳。在这种情况下,您应该使用 Adapter 模式。对不起,如果这听起来有点刺耳,但在判断继承之前尝试使用一些现实生活中的库,如 Qt。继承让生活更轻松。
听起来一点也不刺耳。形状示例如何脱离现实?您能否举一个使用 Adapter 模式修复 Circle 的示例(可能在 ideone 上)?我有兴趣看到它的优点。
那时它并没有脱离现实。当 A 公司收购 B 公司并想将 B 公司的代码库整合到 A 公司时,你有两个完全独立的代码库。想象一下,每个都有不同类型的 Shape 层次结构。您不能轻松地将它们与继承结合起来,并且添加公司 C 并且您会一团糟。我认为您应该观看此演讲:youtube.com/watch?v=0I0FD3N5cgM我的答案较旧,但您会看到相似之处。您不必一直重新实现所有内容,您可以在接口中提供实现,并在可用时选择成员函数。
我看过部分视频,这是完全错误的。除了调试目的,我从不使用 dynamic_cast。动态演员表意味着您的设计有问题,并且此视频中的设计在设计上是错误的 :)。 Guy 甚至提到了 Qt,但即使在这里他也错了——QLayout 既不是从 QWidget 继承的,也不是相反的!
正确的。问题是我不明白为什么继承是“万恶之源”。这样的说法是荒谬的。
R
Rodyland

上面所有的好答案。您应该记住的另一件事 - 您还可以拥有一个纯虚拟析构函数。唯一的区别是您仍然需要实现它。

使困惑?


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

你想要这样做的主要原因是如果你想提供接口方法,就像我一样,但要覆盖它们是可选的。

要使类成为接口类,需要纯虚方法,但所有虚方法都有默认实现,因此唯一可以制作纯虚的方法是析构函数。

在派生类中重新实现析构函数没什么大不了的——我总是在派生类中重新实现析构函数,无论是否虚拟。


为什么,哦,为什么,在这种情况下,有人想让 dtor 成为纯虚拟的吗?那会有什么好处呢?您只需将一些东西强加到派生类上,它们可能不需要包含 - 一个 dtor。
更新了我的答案以回答您的问题。纯虚拟析构函数是实现(实现的唯一方法?)一个所有方法都有默认实现的接口类的有效方法。
L
Luc Hermitte

您还可以考虑使用 NVI(非虚拟接口模式)实现的合同类。例如:

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1() = default;
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    void do_f(Parameters p) override; // From contract 1.
    void do_g(Parameters p) override; // From contract 2.
};

对于其他读者,Jim Hyslop 和 Herb Sutter 撰写的 Dr Dobbs article“对话:几乎是你的”更详细地阐述了为什么人们可能想要使用 NVI。
还有this article Herb Sutter 的“Virtuality”。
如今,“对话:虚拟你的”很难用谷歌搜索,甚至虚拟文章中的参考资料也已死。感谢您的链接@user2067021
C
Cody Gray

如果您使用的是 Microsoft 的 C++ 编译器,则可以执行以下操作:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

我喜欢这种方法,因为它会产生更小的接口代码,并且生成的代码大小可以显着更小。 novtable 的使用删除了对该类中 vtable 指针的所有引用,因此您永远不能直接实例化它。请参阅此处的文档 - novtable


我不太明白您为什么使用 novtable 而不是标准 virtual void Bar() = 0;
它的补充(我刚刚注意到我添加的缺少的 = 0;)。如果您不理解,请阅读文档。
我在没有 = 0; 的情况下阅读了它,并认为这只是一种非标准的完全相同的方式。
U
Uri

对上面写的内容做一点补充:

首先,确保你的析构函数也是纯虚拟的

其次,您可能希望在实施时虚拟(而不是通常)继承,只是为了采取良好的措施。


我喜欢虚拟继承,因为从概念上讲,它意味着被继承类只有一个实例。诚然,这里的课程没有任何空间要求,因此可能是多余的。我有一段时间没有在 C++ 中完成 MI,但非虚拟继承不会使向上转换复杂化吗?
为什么,哦,为什么,在这种情况下,有人想让 dtor 成为纯虚拟的吗?那会有什么好处呢?您只需将一些东西强加到派生类上,它们可能不需要包含 - 一个 dtor。
如果存在通过指向接口的指针销毁对象的情况,则应确保析构函数是虚拟的...
纯虚析构函数没有任何问题。这不是必须的,但它没有任何问题。在派生类中实现析构函数对于该类的实现者来说几乎不是一个巨大的负担。请参阅下面的答案,了解您为什么要这样做。
+1 用于虚拟继承,因为对于接口,类更有可能从两个或多个路径派生接口。我在接口中选择受保护的析构函数。
N
Nathan Xabedi

在 C++20 中,您可以使用 concept 代替类。它比继承更有效。

template <class T>
concept MyInterface = requires (T t) {
    { t.interfaceMethod() };
};

class Implementation {
public:
    void interfaceMethod();
};
static_assert(MyInterface<Implementation>);

然后你可以在一个函数中使用它:

void myFunction(MyInterface auto& arg);

限制是您不能在容器中使用它。


也许您应该解释接口与概念的不同之处,因为问题是关于接口而不是 C++20 中的概念。您应该首先解释语法以及如何使用它,恕我直言。
l
lorro

虽然 virtual 确实是定义接口的事实标准,但我们不要忘记经典的类 C 模式,它带有 C++ 中的构造函数:

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

这样做的好处是您可以重新绑定事件运行时而不必再次构造您的类(因为 C++ 没有用于更改多态类型的语法,这是变色龙类的解决方法)。

提示:

您可以从 this 作为基类继承(允许虚拟和非虚拟)并在后代的构造函数中填写 click 。

您可能将函数指针作为受保护的成员并具有公共引用和/或 getter。

如上所述,这允许您在运行时切换实现。因此,它也是一种管理状态的方法。根据代码中 ifs 与状态更改的数量,这可能比 switch()es 或 ifs 更快(周转预计在 3-4 个 ifs 左右,但始终先测量。

如果您选择 std::function<> 而不是函数指针,您可能能够在 IBase 中管理所有对象数据。从这一点开始,您可以获得 IBase 的值原理图(例如,std::vector 将起作用)。请注意,这可能会更慢,具体取决于您的编译器和 STL 代码;此外,与函数指针甚至虚函数相比,std::function<> 的当前实现往往会产生开销(这在未来可能会改变)。


Y
Yeo

我还是 C++ 开发的新手。我从 Visual Studio (VS) 开始。

然而,似乎没有人提到 VS (.NET) 中的 __interface。我不是很确定这是否是声明接口的好方法。但它似乎提供了额外的强制执行(在 the documents 中提到)。这样您就不必明确指定 virtual TYPE Method() = 0;,因为它会自动转换。

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};

但是,我不使用它,因为我担心跨平台编译兼容性,因为它仅在 .NET 下可用。

如果有人对此有任何有趣的事情,请分享。 :-)

谢谢。


r
rustyhu

如果您只想要接口的静态绑定(没有虚拟,没有接口类型本身的实例,接口仅作为指导):

#include <iostream>
#include <string>

// Static binding interface
// Notice: instantiation of this interface should be usefuless and forbidden.
class IBase {
 protected:
  IBase() = default;
  ~IBase() = default;

 public:
  // Methods that must be implemented by the derived class
  void behaviorA();
  void behaviorB();

  void behaviorC() {
    std::cout << "This is an interface default implementation of bC().\n";
  };
};

class CCom : public IBase {
  std::string name_;

 public:
  void behaviorA() { std::cout << "CCom bA called.\n"; };
};

class CDept : public IBase {
  int ele_;

 public:
  void behaviorB() { std::cout << "CDept bB called.\n"; };
  void behaviorC() {
    // Overwrite the interface default implementation
    std::cout << "CDept bC called.\n";
    IBase::behaviorC();
  };
};

int main(void) {
  // Forbid the instantiation of the interface type itself.
  // GCC error: ‘constexpr IBase::IBase()’ is protected within this context
  // IBase o;

  CCom acom;
  // If you want to use these interface methods, you need to implement them in
  // your derived class. This is controled by the interface definition.
  acom.behaviorA();
  // ld: undefined reference to `IBase::behaviorB()'
  // acom.behaviorB();
  acom.behaviorC();

  CDept adept;
  // adept.behaviorA();
  adept.behaviorB();
  adept.behaviorC();
  // adept.IBase::behaviorC();
}

C
Chen Li

这是 c++ 标准中 abstract class 的定义

n4687

13.4.2

抽象类是只能用作其他类的基类的类;抽象类的任何对象都不能被创建,除非是从它派生的类的子对象。如果一个类至少有一个纯虚函数,那么它就是抽象的。


h
hims
class Shape 
{
public:
   // pure virtual function providing interface framework.
   virtual int getArea() = 0;
   void setWidth(int w)
   {
      width = w;
   }
   void setHeight(int h)
   {
      height = h;
   }
protected:
    int width;
    int height;
};

class Rectangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height); 
    }
};
class Triangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height)/2; 
    }
};

int main(void)
{
     Rectangle Rect;
     Triangle  Tri;

     Rect.setWidth(5);
     Rect.setHeight(7);

     cout << "Rectangle area: " << Rect.getArea() << endl;

     Tri.setWidth(5);
     Tri.setHeight(7);

     cout << "Triangle area: " << Tri.getArea() << endl; 

     return 0;
}

结果:矩形面积:35 三角形面积:17

我们已经看到一个抽象类如何根据 getArea() 定义一个接口,而另外两个类实现了相同的功能,但使用不同的算法来计算特定于形状的区域。


这不是什么被认为是接口!这只是一个抽象基类,其中有一个需要重写的方法!接口通常是只包含方法定义的对象——其他类在实现接口时必须履行的“契约”。