【问题标题】:C++ non-polymorphic interfaceC++ 非多态接口
【发布时间】:2012-08-19 19:06:02
【问题描述】:

简单地说,如何在 C++ 中为单级继承创建接口(出于简单和教学的原因)?我看到一些代码没有使用多态性,但基类包含一个虚拟抽象方法 (virtual void TheMethod() = 0)。

现在从这个抽象类派生了一个类,使用纯虚方法,但是在随后的代码中,使用派生类的实例根本没有动态分配。

这是正确的方法吗?使用多态设计推断的开销如何?

我认为这是不可能的。这看起来更像是隐藏/重影基本方法,即使该方法是纯虚拟方法。

稍后编辑:感谢所有设法提供一些好的答案的人,我想强调一个严重错误,该错误是由于使用“动态分配”而引起的,其含义是强调这一点对象创建可能性是唯一与多态性兼容的可能性。很明显,这不是利用这种运行时调用行为的唯一方法(但可能是最常见的方法?),而是进一步澄清我最初的问题:

有没有办法强制程序员在不使用纯虚方法的情况下实现一个方法?我也许不合理的担心是是否打开了多态设计的大门是否也有点沉重性能方面(每秒对所讨论的方法进行数千次此类调用)。

甚至稍后编辑:使基础具有受保护的构造函数意味着它不能直接实例化(除了使用工厂或其他友好的方式),这可以解决补偿纯虚拟的效果之一方法诱导。但是如何确保任何派生类仍然提供自己的方法实现呢?如果对关联的 vtable 的担心可能有点过分,真的没什么大不了的,我会坚持使用纯虚拟方法(因为 SFINAE 奇怪的重复模板模式更难被不熟悉的人阅读和理解)最少的中级 C++ 程序员 - 像我一样 :) )。

【问题讨论】:

  • 动态分配与 OOP 尤其是多态无关。
  • 我不确定我是否理解这个问题,但即使没有动态分配,也可以在需要对基类的指针/引用的地方使用对这个派生类的指针/引用。此外,这种情况下的开销可能非常小(每个实例最多一个额外的指针,在某些函数调用中还有一些额外的间接)。
  • @Lyth 是也不是。使用多态对象的重点是直到运行时才知道对象的类型,这几乎需要动态分配。
  • @JamesKanze:也是,不是。即使在编译时知道实际类型,使用动态绑定也是有意义的:替代方法是使用模板,但您可能希望在单独的编译单元中拥有东西,并且仍然能够处理满足给定的多个类型界面。
  • @JamesKanze 对象的多态行为不需要动态分配。无论派生对象是如何创建的或它们驻留在何处,您都可以通过基类对它们进行操作,而无需了解任何实现细节(考虑各种对象的静态池)。

标签: c++ inheritance polymorphism virtual


【解决方案1】:

出于教学原因,如果目标是了解如何实施 C++ 中的多态对象,并测试一个已创建的类型,动态的 不需要分配。然而,在实际应用中,它可能 将是,因为使用多态性的主要原因是因为 具体类型要到运行时才能知道。

请注意,在 C++ 中(以及几乎所有其他支持 it),多态性需要引用语义,而不是值 C++ 默认使用的语义。通常,一个类被设计为 用作基类将支持复制和赋值(除了 可能通过虚拟clone() 函数)。

关于开销:与什么相比?调用虚拟 函数通常比调用非虚拟函数更昂贵 功能。但是如果你使用虚函数,那是因为你需要 运行时调度;使用其他一些机制来模拟这个很可能 更贵。

【讨论】:

  • 虚函数最大的成本不是指针间接本身,而是防止内联的机会成本。
  • @rhalbersma 如果函数可以内联,否则。在很多情况下,你说的是真的,但这也取决于架构。在旧的 HP-PA 上,一个虚函数调用清除了管道,这使得它非常昂贵。
【解决方案2】:

您不必动态分配对象以多态地使用它:

struct base {
    virtual void foo() = 0;
};

struct derived : base {
    virtual void foo() {
        // do stuff
    }
};

void f(base& object) {
    object.foo();
}

int main() {
    derived object; // no dynamic allocation at all
    f(object); // polymorphism happens here
}

【讨论】:

  • f 的签名使用引用这一事实,以某种方式涉及动态机制,因此是多态使用,还是我得到错误的图片?我应该声明,不能使用指针或引用来实现多态效果。只是为了一个非常简单的目的堆叠分配的对象。唉,对于简单的场景来说,开销也许真的没什么大不了的。
  • @toedron:如果没有使用指针或引用,那么同意您没有使用运行时多态性。例如,derived object; object.foo(); 的行为就像 foo 是一个非虚拟函数一样。它很可能也会执行,就好像foo 是一个非虚拟函数一样。像这样调用foo 并没有什么“不正确”的地方,但是如果basederived 类只打算以这种方式使用,那么在base 中将foo 设为虚拟是没有意义的,就此而言,base 的存在毫无意义。 base 有没有调用过纯虚函数?
  • ... 因为如果是这样,请记住 this 是一个指针,而 base 那么确实是通过 this 使用运行时多态性。
  • 这在 Doom3 源代码以及 Valve 的 Source 引擎中做了很多工作(尽管它们倾向于使用指针而不是引用)。他们这样做是出于性能原因以避免 vtable 查找。这里有进一步的讨论:reddit.com/r/cpp/comments/x3bqg/doom_3_vtable_trick
  • @SteveJessop 是的,我在谈论一个非常具体的场景,其中尽可能不使用指针或引用来强制执行多态解释。我的目标是实现接口,但不一定依赖于虚函数及其真正含义。正如我目前所知,如果没有 virtual 方法说明符,就无法声明抽象类。我在项目的代码库中看到了数十个具有虚拟方法的类,这些类没有使用多态性。当然,这并不意味着第三方不能以其他方式使用该类。
【解决方案3】:

是的,正如其他人所说,您基本上使用具有纯虚拟(抽象)成员函数且没有数据成员的类。在实现这个接口的时候,自然要提供这些方法。

另一方面,这与动态分配无关。您是否拥有自动对象(即堆栈)或动态对象(即堆)与您如何使用它们无关,包括多态性。你的意思是动态绑定

现在,说了这么多,您可以在不使用模板的动态绑定(即“多态性”)的情况下实现接口。基本上,您将使用 SFINAE+CRTP 通过从模板类私有继承来检查给定的成员函数是否存在。基本上,您的父类(不包含虚拟成员)template <typename T> class FooIface;(继承自class Foo : private FooIface<Foo>)将通过尝试调用来确保T 具有成员函数foo。使用元编程技巧,您还可以确保 foo 具有正确的类型。

但这可能太麻烦且难以阅读。抽象基类是常用的方法。

【讨论】:

  • 目前这是最接近答案的事情,尽管其余答案也提供了关于可以做什么和应该做什么的令人满意的见解。模板方式只是我愚蠢的 cpp 难题的理想解决方案
【解决方案4】:

C++中没有接口的概念,
您只能模拟使用 Abstract class 的行为。
抽象类是一个具有至少一个纯虚函数的类,一个不能创建抽象类的任何实例,但您可以创建指向它的指针和引用。此外,从抽象类继承的每个类都必须实现纯虚函数才能创建它的实例。

动态分配多态不相关!

动态分配定义在哪里对象将被分配,并且该对象应具有显式的内存管理,不像为自动变量提供的隐式内存管理。

多态性表示一个事物的多种形式。那是在类中具有不同行为的同一个函数。您可以有一个指向堆栈上的对象的基类指针,并且仍然具有多态行为。

【讨论】:

  • interface 的概念独立于语言(不应与某些语言中使用的关键字interface 混淆)。事实上,C++ 对接口概念的支持比许多具有关键字的语言要好得多。实际上,如果直到运行时才知道确切的类型,则必须动态分配对象(并且必须使用引用语义——多态对象类型通常不支持复制和赋值)。跨度>
  • @JamesKanze:我同意 interface 独立于语言的概念。此外,在实践中确实 Polymorphism needs Dynamic Allocation,但与 OP 的混淆似乎混淆了 need i> 与 necessity 并非如此,因此强调两个概念不相关。
  • 也许更正确的表述是导致你使用多态的原因也会导致你使用动态分配。抽象概念不相关(我认为您想说的是),但在实践中,两者往往会同时出现,因为相同的原因会激发两者。
  • @JamesKanze:我想标准中的反例是 iostreams 库,它是多态的,但通常不是动态分配的。实现ostream &operator<<(ostream &) 可能在某种意义上比实现template <typename STREAM> STREAM &operator<<(STREAM &) 更好。再说一次,这个库并没有被很多人认为是 C++ 设计中的最佳实践(我自己的示例 operator<< 是有问题的,因为它返回与其参数不同的类型),所以...
  • @SteveJessop istreamostream不是多态的;没有一个成员函数是虚拟的。 std::streambuf 是多态的,您可能对此有所了解;在早期的实现中,它们通常是动态分配的,但现在更常见的是它们是派生 [io]stream 类的成员或私有基类。然而,这似乎是一个例外。
猜你喜欢
  • 1970-01-01
  • 2017-09-11
  • 1970-01-01
  • 2012-06-25
  • 2011-09-24
  • 1970-01-01
  • 2021-12-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多