【问题标题】:Once a function name has been declared as 'virtual', can it ever be be made un-virtual again?一旦函数名称被声明为“虚拟”,它是否可以再次变为非虚拟?
【发布时间】:2011-07-28 21:57:37
【问题描述】:

假设我有一些类链,其中每个类都派生自它之前的类。无论出于何种原因,他们都喜欢为某些成员函数使用相同的名称。有点像这样:

class C1 { public:             void f() { cout<<"C1"; }; };
class C2 : public C1 { public: void f() { cout<<"C2"; }; };
class C3 : public C2 { public: void f() { cout<<"C3"; }; };

显然,如果我只是声明一些对象,然后从它们中调用函数 f ,都会调用与它们各自的对象类型相关联的函数:

C1 c1; c1.f(); // prints C1
C2 c2; c2.f(); // prints C2
C3 c3; c3.f(); // prints C3

现在,如果我声明一些指向对象的指针,然后从它们调用函数 f,所有将调用与它们各自的指针类型相关联的函数:

C1* p1 = &c1; p1->f(); // prints C1
C1* p2 = &c2; p2->f(); // prints C1
C1* p3 = &c3; p3->f(); // prints C1
C2* p4 = &c2; p4->f(); // prints C2
C2* p5 = &c3; p5->f(); // prints C2
C3* p6 = &c3; p6->f(); // prints C3

这一切都很棒。我要么调用与对象类型关联的函数,要么调用与指针类型关联的函数...

当然,我也可以将功能设为“虚拟”。然后,如果我从某个对象调用该函数,我的行为不会发生任何变化;但是,如果我从某个指针调用该函数,那么我将不仅仅调用该指针类型的函数,我实际上将调用该指针指向的对象类型的函数。到目前为止一切顺利。

我什至可以在继承链的中途对虚拟进行更改。假设我在 C2 类的函数 f 之前放置了一个 virtual。现在该函数已变为虚拟(即,当从指针调用时,它使用指向对象的类型而不是指针类型来解析函数调用),不仅适用于它自己的类,而且适用于所有未来的类来源于它。

我的问题是:一旦函数被声明为虚拟(在继承链中的某个点),它是否可以恢复为非虚拟(在继承链的更下方)?

澄清一下:当我说恢复到非虚拟行为时,我的意思是当我从指针调用函数时,它将使用指针的类型来解析函数调用(而不是指针的对象类型指向)。

【问题讨论】:

  • C1* p1 = &amp;C1; 等,你的意思一定是C1* p1 = &amp;c1;(即你不尝试获取班级的地址)。
  • 注意,您不能在链的中途使函数成为虚拟函数。您隐藏了原始功能并引入了一个新功能。您只会在该点以下获得多态行为;在它上面你会得到静态调度。
  • @Jimmy:当你说“reverted back”时,你期望什么行为?
  • @Dennis:这就是我所说的“在链的中途对虚拟进行更改”的意思——在引入关键字 virtual 之前,该函数将对所有类进行静态行为,并且然后使用 virtual 关键字对类以及从该类派生的所有类执行多态操作。这是可能的,是吗?
  • @larsmans:好电话,我对剪切和粘贴有点疯狂。固定。

标签: c++ virtual


【解决方案1】:

一旦函数被声明为虚拟(在继承链中的某个点),它是否可以恢复为非虚拟(在继承链的更下方)?

没有。一旦在继承线的某处创建了函数签名virtual,它将保持这种状态:派生类中具有相同签名的每个函数也将是virtual

解决此问题的一种方法是使用(滥用?)template method pattern

struct Base {
    virtual void doFoo() { bar(); }
    void foo() { doFoo(); }
};

struct Derived1 : public Base {
    virtual void doFoo() { baz(); }  // "overrides" foo via doFoo
};

struct Derived2 : public Base {
    void foo() { quux(); }  // "un-virtualize" foo by decoupling it from doFoo
};

在这个继承树中,指针类型将决定调用哪个foo;如果是来自Base 的对象,则指向对象的类型将决定调用哪个doFoo

【讨论】:

  • C++0x 不是添加了final 关键字来取消虚拟化函数吗?
  • @Kerrek SB:IIUC,final 完全禁止在基类中使用签名。
  • 呃……这是什么意思? :-)
  • 如果你有一个指向 Derived2 的 Base* 指针,则对 foo() 不起作用。
  • @yi_H:它的工作原理完全符合我对非virtual 成员函数的期望:调用Base::Foo
【解决方案2】:

简单的回答:没有。

如果您知道虚函数在 C++ 中的工作原理,您就会知道这是不可能的。简单地说,每个类都有一个表,其中包含一个虚函数列表和被覆盖函数的地址。当您使用 virtual 覆盖函数时,该函数将列在声明它的类的表中的该列表中,因此它将在继承链的其余部分中存在。

【讨论】:

  • vtables 的讨论是一个无关紧要的实现细节。 C++ 可以有一个关键字,比如real,覆盖virtual 的含义,编译器将有足够的信息来决定每个成员函数声明是否应该是virtual
  • 错误答案:使用实现细节作为为什么某事有效的论据并不是真正的正当理由。不使用这种技术来实现虚函数的实现会发生什么。您需要根据语言的规范(不是特定的实现)提供参数。
  • 我也在想同样的事情 :)
  • 一般情况下是这样。但是第一个标准是从 98 开始的,它受到了 Stroustrup(83) 制定的语言本身的影响,以及 Stroustrup 以这种方式实现语言的事实(为了应对包括超市原则和 C 向后兼容性在内的语言原则) ) 触发了 C++ 没有关键字 real 的事实。你说的好像 C++ 首先是标准化的,然后是实现的。相信我,如果他在不违反他的超市原则和 C 向后兼容原则的情况下找到一种方法来允许它,我们今天就会真正实现
  • 即使使用 vtbls,实现起来也很简单。对于假设的real 的所有“下游”类,它们的f() 的vtbl 条目将简单地指向声明它real 的超类的f()
【解决方案3】:

简单的答案:不。

一旦它被设为虚拟,它将在所有派生类中都是虚拟的(即使以后不使用 virtual 关键字)。

出于这个原因,一些较新的语言强制您使用另一个关键字,以便您知道您正在覆盖虚拟方法(因为它可能并不明显),但没有一种允许您取消虚拟方法。

【讨论】:

  • 好吧,Java 提供了final。意思不太一样,但很接近。
  • 我在考虑 C# 覆盖
【解决方案4】:

正如其他人已经说过的,你不能对函数进行去虚拟化,但绝对没有什么可以阻止你调用你需要的函数的特定版本,只要它对底层类是合法的:

C1* p1 = &c1; p1->C1::f(); // prints C1
C1* p2 = &c2; p2->C1::f(); // prints C1, not C2
C1* p3 = &c3; p3->C1::f(); // prints C1, not C3
C2* p4 = &c2; p4->C2::f(); // prints C2
C2* p5 = &c3; p5->C2::f(); // prints C2, not C3
C3* p6 = &c3; p6->C3::f(); // prints C3

【讨论】:

    【解决方案5】:

    没有。

    用模板方法解决这个问题:

    class C1 { public:  virtual void f() { g(); }; void g() { std::cout<<"C1"; }; };
    class C2 : public C1 { public: virtual void f() { std::cout<<"C2"; }; };
    class C3 : public C2 { public: virtual void f() { g(); }};
    

    现在 C3 及其后代具有 C1 的行为。

    【讨论】:

    • 这里没有什么“非虚拟化”,恕我直言,因为C3 *p = new C4; p-&gt;f(); 将从struct C4 : public C3 { void f() { bla(); } }; 调用f。这也与模板方法模式无关。不过,我必须承认 OP 的要求相当模糊。
    【解决方案6】:

    不确定,它会回答你 - 你可以在 VC++ 中使用novtable 属性。

    【讨论】:

      【解决方案7】:

      不,不能恢复。我从来不需要这个,也无法想象现实生活中需要这个的类层次结构示例。

      另外,建议避免使用较深的层次结构,例如由 B.Straustrup 撰写。

      【讨论】:

      • 只是探索语言的极限,我相信 Bjarne 会同意的。
      • 我真的很怀疑。你在实际项目中不需要这个,对吧?我也是。
      • 顺便说一句,这是我最不喜欢未注释的反对票的情况。现在我不知道是什么原因:当这个功能有用或者它只是我的活泼风格时,downvoter 经常遇到示例。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-24
      • 2011-08-02
      • 2012-03-09
      • 2016-02-05
      • 1970-01-01
      • 1970-01-01
      • 2021-09-13
      相关资源
      最近更新 更多