C++中`override`/`final`关键字的反义词是什么?

9
在c++11中,override修饰符可以防止不意外地覆盖基类中的虚函数(因为签名不匹配)。final修饰符可以防止无意中覆盖派生类中的函数。是否有一个修饰符(例如firstno_override)可以防止覆盖未知的基函数?我希望在基类中添加具有与派生类中已存在的虚函数相同签名的虚函数时,编译器会报错。以下是一个抽象类B和其子类C的示例,当类A添加了新的虚函数时,B中的showPath()将显示错误的内容。为了避免这种情况,应该重命名B::showPath()并实现B::showPath()的覆盖。
#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(链接可能已过期)


4
也许使用“-Wsuggest-override” GCC选项可以帮助,详见https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html。 - gavv
1
在我看来,这更多是一个关于设计的问题。正如Bathsheba所提到的,除了在基类中使用final之外,没有其他可能性。或者您可以使用Leon提到的技巧,即打破接口类的目的([请参见此处](http://coliru.stacked-crooked.com/a/17346a88fa6aaa89)),但这会带来其他一些问题。正如您在最后一次编辑中提到的那样,您不应该使用不同含义的相同函数名称。如果您想获得信息,请使用一些分析工具(在我看来,这是唯一正确的答案,也是Bathsheba提到的)。 - user1810087
1
假设类A与派生类在不同的头文件中,你可以使用类似于#define SetPath #error #include "classA.h" #undef SetPath的东西。这将防止任何SetPath函数被添加到A中,但是在将SetPath添加到A之前,它将无错误地构建。我并不是说这很好。如果我把它作为答案,我希望有些人会因为它使用了#define而对其进行投票。 - ROX
1
你用新的代码创建了更多的混乱! :-) 每次你改变代码,都会使我们答案中从你的问题中提取的例子变得无效。为什么要使用“B的实例”这个术语?这会造成困惑,因为B由于是纯虚函数而不能有实例。另外,PainterPath在这里有什么作用?你最后的陈述很令人困惑。我建议你不要放置实际的带构造函数等代码,只需放置独立的最小部分,任何人都可以使用ideone.com进行验证。你也可以看一下我的答案和评论,如果它们不能满足你的需求。 - iammilind
2
@iammilind:“为什么要使用“B的实例”这个术语?这很容易让人困惑,因为B由于是纯虚类而不能有实例。” 在面向对象编程中,我理解的是子类的实例也是基类的实例。将“B的实例”理解为“B的实例(包括任何派生自B的类)”。 - davmac
显示剩余4条评论
4个回答

3

由于本回答包含了其他所有回答,因此这是社区 wiki。如果有其他回答对你有帮助,请另外点赞。

  1. 不,没有像firstno_override这样的限定符。(回答)
  2. 应尽可能使用override限定符。
    如果可用,Qt有一个Q_DECL_OVERRIDE,它会扩展为override
    如果不可用,则至少在每个重载函数中标记一个注释。
  3. 如果这样做,存在编译器标志可以警告缺少override
    "现在Clang有-Winconsistent-missing-override,而更新的GCC有-Wsuggest-override"
    我不知道有没有VS2012的标志。请随意编辑。
  4. 您可以通过添加基类无法知道的“秘密”来模拟所需的行为。 (回答)
    这对于非常特定的用例很有帮助,但通常会破坏虚拟性概念(请参见其他回答中的注释)。
  5. 如果您不拥有基类并且存在冲突(例如编译器警告),则需要在所有派生类中重命名虚拟函数。
  6. 如果您拥有基类,则可以暂时final添加到任何新的虚拟函数中。(回答)
    在代码无错误地编译后,您就知道该名称和签名的函数不存在于任何派生类中,然后可以再次删除final

...我想我会将first虚拟函数标记为DECL_FIRST。也许未来会有一种与编译器无关的检查方式。


3
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 修饰符继续进行。

1
@user1810087,OP想要在基类中添加新方法,并打算在意外情况下与任何派生类相同时获得编译器错误。因此,当我们使用“final”修饰符添加新方法时,派生类的后续签名自动成为其种类的“第一个”。如果相同,则编译器会报错。 - iammilind
不,他希望在基类添加方法时出现编译器错误,而不是在他自己添加方法时出现错误。 - user1810087
@user1810087,无论是OP还是其他程序员在基类中添加方法,都必须使用final修饰符进行添加,如果他们关心后续的派生。如果他们打算添加可被重写的方法,那么final是不需要的,并且匹配签名的后果必须协商解决。 - iammilind
2
我同意使用final通常是解决这个问题的方法 - 让基类负责检查,但OP知道final并且似乎正在寻求一种不需要基类使用final的替代方法,并将责任放在派生类上。 - ROX
1
@ROX,实际上我刚刚编辑了我的答案。final关键字不是永久性的。它只是为了确保:“我正在添加一个新方法,我不想手动检查所有派生类是否存在这样的方法,所以请自动快速报告给我。”类比地说,这就像在g++中使用-g选项编译代码时需要调试堆栈跟踪一样。一旦调试完成并修复了错误,我们就会删除-g并重新编译代码。 - iammilind
但是现在B::fooHasBeenDone()没有被实现,也无法使用。 - user1810087

2

不,没有。

在基类中添加一个与子类中的虚函数具有相同签名的虚函数不会破坏任何现有功能,除非将该虚函数添加到基类会使其变成多态类型。因此,在标准情况下,它是良性的,并且纯粹主义者会争辩说,添加语言特性来防止这种情况实际上是毫无意义的。

(当然,您可以将新函数标记为final,以检查子类函数是否会覆盖它。)

您唯一的选择是使用代码分析工具。

(请注意,尽管VS2012具有某些C++11标准的功能,但它并未实现甚至声称实现C++11标准。)


2
它怎么可能不会破坏现有功能呢?过载的虚函数可能在意料之外或无效的情况下执行。 - Martin Hennings
2
什么时候?我想不出任何情况。请提交一个反例(当然还要点个踩)。这可能会治好我的宿醉。 - Bathsheba
3
请提交一个反例。请访问https://dev59.com/sJnga4cB1Zd3GeqPXmDn - Leon
1
@LightnessRacesinOrbit https://dev59.com/sJnga4cB1Zd3GeqPXmDn#38658152 在基类中使用或不使用virtual均可以编译,但会改变行为。 - Jonathan Wakely
1
@Bathsheba,基类可能已经是多态的了,但你仍然面临着同样的问题。基类中的其他不相关虚函数对Leon的示例(我的示例非常相似)没有影响。 - Jonathan Wakely
显示剩余11条评论

1

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())
    {
    }
};

如果您想要引入一个新的虚函数,那么请按照以下方式实现:
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())
    {
    }
};

1
这有潜力,但是 foo 需要是虚函数。也许将结构体设为 protected?如果你明白我的意思,protected 的作用是“向下”的。 - Bathsheba
// 这个函数永远不会覆盖基类中的函数,因为它在基类中无法被声明为虚函数! - user1810087
@user1810087 它可以是虚拟的,但是DontOverride必须被设置为protected,这样子类才能覆盖它。 - Leon
3
尽管基类中不存在与相同签名的foo函数,因此Derived::foo不会覆盖任何基类函数,但给虚函数参数设置默认值意味着调用Derived::foo()时没有传递参数,将会调用Derived::foo,而实际上可能本意是要调用Base::foo() —— 因为foo也是一个新添加到Base中、没有参数(在此示例中)的虚函数。总体效果仍然是不会调用新的基类函数。 - ROX
1
我在这里支持@ROX。Derived::foo隐藏了重载函数Base::foo,这意味着调用foo(通过指向Derived的Base指针)将调用Base::foo,而调用Derived::foo需要一个Derived指针,这破坏了接口类的整个目的。 - user1810087
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接