【问题标题】:What is the opposite of c++ `override` / `final` specifier?c++ `override` / `final` 说明符的对立面是什么?
【发布时间】:2016-12-04 00:44:27
【问题描述】:
  • 在 c++11 中,override specifier 防止不覆盖预期的虚拟基函数(因为签名不匹配)。
  • final specifier 可防止无意中覆盖派生类中的函数。

=> 是否有一个说明符(可能是firstno_override)来防止覆盖未知的基函数?

当一个虚函数被添加到一个与派生类中已经存在的虚函数具有相同签名的基类时,我想得到一个编译器错误。


编辑 4:为了让这个问题简单且答案相关,这里又是

原始伪代码

  • 抽象的class B : Aprivate: virtual void fooHasBeenDone() = 0;
  • class C : B 实现 private: virtual void fooHasBeenDone() override { react(); }
  • 现在class A 得到一个新的private: virtual void fooHasBeenDone();
  • 但新的A::foo 可能与原来的B::foo 不同。

还有一个具体的例子

  • abstract class B : Avirtual void showPath() = 0; 表示 PainterPath
  • class C : B 实现 virtual void showPath() override { mPath.setVisible(); }
  • 现在class A 得到一个新的virtual void showPath(); 表示文件路径
  • 现在,当 A 调用 showPath() 时,B 会显示painterPath 而不是某个文件路径。

当然这是错误的,然后我应该将B::showPath() 重命名为B::showPainterPath() 并同时实现B::showPath() override。我只是想从编译器那里得到通知。


这是一个编译真实世界的例子

#include <iostream>
#define A_WITH_SHOWPATH

class A
{
#ifdef A_WITH_SHOWPATH
public:
    void setPath(std::string const &filepath) {
        std::cout << "File path set to '" << filepath << "'. Display it:\n";
        showPath();
    }
    // to be called from outside, supposed to display file path
    virtual void showPath() {
        std::cout << "Displaying not implemented.\n";
    }
#else
    // has no showPath() function
#endif  
};

class B : public A
{
public:
    virtual void showPath() = 0; // to be called from outside
};

class C1 : public B {
public:
    virtual void showPath() override {
        std::cout << "C1 showing painter path as graphic\n";
    }
};

class C2 : public B {
public:
    virtual void showPath() override {
        std::cout << "C2 showing painter path as widget\n";
    }
};


int main() {
    B* b1 = new C1();
    B* b2 = new C2();

    std::cout << "Should say 'C1 showing painter path as graphic':\n";
    b1->showPath();
    std::cout << "---------------------------\n";
    std::cout << "Should say 'C2 showing painter path as widget':\n";
    b2->showPath();
    std::cout << "---------------------------\n";

#ifdef A_WITH_SHOWPATH
    std::cout << "Should give compiler warning\n or say \"File path set to 'Test'. Display it:\"\n and \"Displaying not implemented.\",\n but not \"C1 showing painter path as graphic\":\n";
    b1->setPath("Test");
    std::cout << "# Calling setPath(\"Test\") on a B pointer now also displays the\n#  PainterPath, which is not the intended behavior.\n";
    std::cout << "# The setPath() function in B should be marked to never override\n#  any function from the base class.\n";
    std::cout << "---------------------------\n";
#endif
    return 0;
}

运行它并查看文本输出。


作为参考,具有特定用例(PainterPath 实例)的旧示例:

https://ideone.com/6q0cPD(链接可能已过期)

【问题讨论】:

  • 也许-Wsuggest-override GCC 选项可能会有所帮助,请参阅gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
  • 恕我直言,这是一个更多关于设计的问题。正如 Bathsheba 提到的,除了在基类中使用 final 是不可能的。或者您可以使用 Leon 提到的技巧,它破坏了接口类目的 (see here) 并解决了其他一些问题。正如您在上次编辑中提到的那样,您不应该将相同的函数命名为不同的含义。如果您想获得通知,请使用一些分析工具(Bathsheba imho 也提到过这里唯一的正确答案)。 (恕我直言)
  • 假设 A 类位于派生类的单独标头中,您可以使用 #define SetPath #error #include "classA.h" #undef SetPath 之类的内容。这将阻止将任何 SetPath 函数添加到 A,但会在将 SetPath 添加到 A 之前构建而不会出错。我并不是说它很好。如果我把它作为一个答案,我希望有些人会因为它使用#define 而投反对票。
  • 您对新代码造成了更多混乱! :-) 每次您更改代码时,都会使我们回答中从您的 Q 中获取的示例无效。为什么使用术语“B 的实例”?它令人困惑,因为 B 由于纯 virtual 而不能有实例。另外,PainterPath 必须在这里做什么?你的最后一句话令人困惑。我建议不要将实际代码与构造函数等放在一起,而只需放置最小的部分,这是独立的,任何人都可以使用 ideone.com 进行验证。如果不满足您的需求,您也可以查看我的答案和评论。
  • @iammilind “你为什么使用术语“B的实例”?它令人困惑,因为B由于纯虚拟而不能有实例” OO中的AFAIU通常是a的实例子类也是基类的一个实例。将“B 的实例”读作“B 的实例(包括从 B 派生的任何类)”。

标签: c++ c++11 inheritance class-hierarchy modifiers


【解决方案1】:

firstno_override 等说明符的功能并不存在。可能是因为它可能会造成混乱。但是,通过改变方法可以轻松实现。

应该使用final 说明符在基类中添加任何新方法。这将有助于获取任何匹配签名的编译器错误。因为,它将使后续派生类方法签名自动成为同类中的“第一个”。稍后可以删除 final 关键字,因为它仅用于“第一手验证”。

在新添加的基本方法之后放置和删除final关键字类似于使用调试(g++ -g)选项编译二进制文件,这有助于您修复错误。在生产环境中,调试选项被移除以进行优化。

从你的例子:

class A {};  // no method, no worry

class B {
  public: virtual void showPath() = 0;  // ok
};
...

现在你不小心在A 中添加了类似的方法,导致错误:

class A {
  public: virtual void showPath() final;  // same signature by chance
  // remove the `final` specifier once the signature is negotiated
}; 
class B {
  public: virtual void showPath() = 0;  // ERROR
};

因此必须协商新 A::showPath() 和现有 B::showPath() 之间的签名,然后通过删除 final 说明符继续。

【讨论】:

  • @user1810087,OP 想要在基类中添加新方法并打算得到一个编译器错误,如果它意外地与任何派生类相同。因此,当我们添加带有final 说明符的新方法时,派生类的后续签名自动成为同类中的“第一个”。如果相同,编译器会报错。
  • 不,当方法添加到基础时,他想要得到编译器错误,而不是他添加方法到基础。
  • @user1810087,在基础中添加方法的人——无论是 OP 还是其他编码器,如果他们关心后续派生,则必须使用 final 说明符添加它。如果他们打算添加要覆盖的方法,那么无论如何,final 不是必需的,并且必须协商匹配签名的后果。
  • 我同意使用 final 通常是执行此操作的方法 - 使基类负责检查,但 OP 知道 final 并且似乎要求使用替代方法不需要基类使用final,而是将责任放在派生类上。
  • @ROX,实际上我刚刚编辑了我的答案。 final 关键字不能永久放置。这只是为了确保:“我正在添加一个新方法,如果存在任何此类方法,我不想手动检查所有派生类,因此请自动给我快速报告。”类似地,它类似于使用 -g 选项在 g++ 中编译代码,您需要在其中调试堆栈跟踪。一旦调试并修复错误,我们删除-g 并重新编译代码。
【解决方案2】:

这个答案是社区 wiki,因为它结合了所有其他答案。请投票赞成对您以及这个有帮助的具体答案。

  1. 不,没有像 firstno_override 这样的说明符。 (answer)
  2. 您应该尽可能多地使用override 说明符。
    Qt 有一个macro Q_DECL_OVERRIDE,如果可用,它会扩展为override
    如果不可用,至少用注释标记每个覆盖函数。
  3. 如果您这样做,编译器标志会警告您缺少 override
    Clang now has -Winconsistent-missing-override, and newer GCCs have -Wsuggest-override.
    我不知道有这方面的 VS2012 标志。随意编辑。
  4. 您可以通过添加基类不知道的“秘密”来模仿所需的行为。 (answer)
    这在非常具体的用例中很有帮助,但通常会打破虚拟性的概念(请参阅其他答案的 cmets)。
  5. 如果您不拥有基类并且存在冲突(例如编译器警告),则需要在所有派生类中重命名您的虚函数。
  6. 如果您拥有基类,您可以暂时final 添加到任何新的虚函数。 (answer)
    代码编译无误后,您知道任何派生类中都不存在该名称和签名的函数,您可以再次删除final

...我想我将开始将 first 虚函数标记为DECL_FIRST。也许将来会有一种独立于编译器的检查方式。

【讨论】:

    【解决方案3】:

    没有。

    将虚函数添加到与子类中的虚函数具有相同签名的基类不能破坏任何现有功能除非添加该虚函数函数将基类变成多态类型。所以在正常情况下,这是良性的,最纯粹的人会争辩说,添加语言特性来防止这种情况是毫无意义的。

    (当然你可以标记你的新函数 final 只是为了检查一个子类函数不会破坏它。)

    您唯一的选择是求助于代码分析工具。

    (请注意,VS2012 没有实现,甚至没有声称实现 C++11 标准,尽管它确实有一些标准。)

    【讨论】:

    • 如何不破坏现有功能?重载的虚函数可能会在意外/无效的情况下执行。
    • 什么时候?我想不出情况。一定要提交一个反例(当然还有一个反对意见)。它可能会治愈我的宿醉。
    • @LightnessRacesinOrbit 查看我的编辑 - Base 可能会在 Derived 未预料到的地方调用 foo()。 (如果 Base 不是我的,我想重命名 Derived::foo() 来解决冲突。)
    • @Bathsheba 提交反例stackoverflow.com/questions/38657534/…
    • @LightnessRacesinOrbit stackoverflow.com/a/38658152/981959 在基类中使用或不使用 virtual 进行编译,但会改变行为。
    【解决方案4】:

    C++ 似乎没有提供这种开箱即用的方法。但是你可以像下面这样模仿它:

    template<class Base>
    class Derived : public Base
    {
    private:
        struct DontOverride {};
    
    public:
        // This function will never override a function from Base
        void foo(DontOverride dummy = DontOverride())
        {
        }
    };
    

    如果你打算引入一个 new 虚函数,那么就按如下方式进行:

    template<class Base>
    class Derived : public Base
    {
    protected:
        struct NewVirtualFunction {};
    
    public:
        // This function will never override a function from Base
        // but can be overriden by subclasses of Derived
        virtual void foo(NewVirtualFunction dummy = NewVirtualFunction())
        {
        }
    };
    

    【讨论】:

    • 这有潜力,但foo 需要是虚拟的。也许制作结构protected?如果你明白我的意思,protectedness “向下”而不是“向上”工作。
    • // This function will never override a function from Base ,因为它在基类中永远不可能是虚拟的!
    • @user1810087 它可以是虚拟的,但必须将 DontOverride 设为保护,以便子类可以覆盖它。
    • 虽然基类中不能存在具有相同签名的 foo 函数,所以 Derived::foo 不会覆盖任何基函数,给 dummy 参数一个默认值确实意味着对 Derived 的调用: :foo() 不带参数将是对 Derived::foo 的调用,其中对 Base::foo() 的调用可能是有意的 - foo 也是添加到 Base 的新虚函数,(在此示例中)没有参数。总体效果仍然是新的基本函数没有被调用。
    • 我在这里是@ROX。 Derived::foo 隐藏了重载函数 Base::foo,它倾向于调用 foo(通过指向 Derived 的 Base 指针)将调用 Base::foo,而调用 Derived::foo 需要是一个 Derived 指针,这破坏了接口类的全部目的。
    猜你喜欢
    • 2017-09-28
    • 2018-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多