C++风格:在重写方法前加上virtual关键字

27
我和同事们在讨论是否应该在覆盖方法时添加virtual关键字,是在原始基类上添加还是都添加。
我倾向于给所有虚拟方法(即涉及vtable查找的方法)都加上virtual关键字。我的理由有三个:
1. 由于C++缺乏一个override关键字,virtual关键字的存在至少会通知你该方法涉及查找,并且理论上可以被进一步专门化覆盖,或者可以通过指向更高基类的指针调用。
2. 始终使用这种风格意味着,当你看到一个没有virtual关键字的方法时(至少在我们的代码中),你可以最初假定它既不是从一个基类派生出来的,也没有在子类中专门化。
3. 如果出现错误,将virtual从IFoo中删除,则所有子项仍将正常运行(CFooSpecialization :: DoBar仍将覆盖CFooBase :: DoBar,而不仅仅是隐藏它)。
反对这种做法的争论,据我所了解,是“但那个方法不是虚拟的”(我认为这是无效的,来源于对虚拟性的误解),以及“当我看到virtual关键字时,我期望意味着某个人正在继承它,并开始搜索他们”。
虚拟类可能分布在几个文件中,有几个专业化。
class IFoo {
public:
    virtual void DoBar() = 0;
    void DoBaz();
};

class CFooBase : public IFoo {
public:
    virtual void DoBar(); // Default implementation
    void DoZap();
};


class CFooSpecialization : public CFooBase {
public:
    virtual void DoBar(); // Specialized implementation
};

从风格上讲,你会从这两个派生类中去掉virtual关键字吗?如果是这样,为什么?Stack Overflow在这方面有什么想法?


Stylistically, would you remove the virtual keyword from the two derived classes? If so, why? What are Stack Overflow's thoughts here?

7
我很感兴趣看到Bjarne(或C++委员会)为什么允许在派生类中省略虚函数。这个理由可能会提供更有说服力的原因来支持省略它,比你的同事所提供的原因更好。虽然这可能仅适用于某些情况。 - Steve Jessop
2
很遗憾,规范中没有包含任何理由。它只是说这是“合法但多余的(具有空语义)”。不过,如果有任何记录说明理由是什么,那将会很有趣。 - Tyler McHenry
我没有《设计与演化》的副本,其中可能包含关于它的任何内容。 - Steve Jessop
3
我想指出的是,从C++11开始,您还可以使用override修饰符来标明一个函数是虚函数并重写另一个函数;这样做也使编译器能够在该函数不覆盖任何东西时给出错误消息。对于您的示例,CFooBaseCFooSpecialization将具有virtual void DoBar() override; - Justin Time - Reinstate Monica
1
@JustinTime 我认为人们想要的,尽管不切实际,是一种标准强制要求使用描述性vfunc声明的方法,而不是必须记住故意输入另一个完全可选的关键字,而无需打开依赖于编译器的警告。但我认为现在已经太晚了,因为它会破坏什么以及可能导致的惯例反弹。话虽如此,这是针对g++的技巧:https://dev59.com/MF4b5IYBdhLWcg3wbRIf - underscore_d
6个回答

23

我完全同意你的理解。这是一个很好的提醒:当调用该方法时,它将具有动态分派语义。你同事使用的“那个方法不是虚函数”的论点完全是无稽之谈。他把虚函数和纯虚函数的概念搞混了。


6

一旦是虚函数,永远是虚函数

因此,如果在后续的类中未使用virtual关键字,则不会阻止函数/方法被“虚拟”,即被覆盖。因此,在我工作过的某个项目中,有以下指南,我有些喜欢:

  • 如果函数/方法应该被覆盖,请始终使用“virtual”关键字。特别是在接口/基类中使用时。
  • 如果派生类应该进一步子类化,请明确说明每个可以被覆盖的函数/方法的“virtual”关键字。C++11使用“override”关键字
  • 如果派生类中的函数/方法不应再次进行子类化,则应注释“virtual”关键字,表示已覆盖该函数/方法,但没有其他类再次覆盖它。当然,这并不能防止在派生类中进行覆盖,除非将类设置为final(不可派生),但它表明该方法不应该被覆盖。 例如:/*virtual*/ void guiFocusEvent(); C++11,与“override”一起使用“final”关键字 例如:void guiFocusEvent() override final;

4

我倾向于不使用编译器允许我省略的任何语法。尽管如此,C# 的设计之一(为了改善 C++)是要求重写虚方法时标记为“override”,这似乎是个合理的想法。但是我的担忧在于,由于它完全是可选的,所以总有一天会有人省略它,而到那时你已经习惯了期望需要指定“virtual”的重写方法。也许最好只是遵循语言的限制。


4
我倾向于不使用编译器允许省略的任何语法,但我认为有一些情况可以使用关键字以增强清晰度。例如,如果我将类成员分组,则即使与上一组具有相同的访问说明符,我也会在每组开头添加一个访问说明符。我将“virtual”放在相同的类别中-在我的经验中,很容易忽略某些东西是虚拟的,因此添加它的价值大于成本。只要基类确实是(并保持)虚拟的,那么它实际上只是一个注释。 - Steve Jessop
1
这是否意味着您不使用“inline”、“const”、“volatile”和“explicit”?请注意,我不会使用旧方法“auto”和“register”(不记得它们是否从C转移到C++,也许没有,但没关系,我20多年来从未使用过它们)。如果您的意图是“我不使用对代码没有影响的任何语法”,那就有点不同了。不想挑剔。 - Dan
2
我经常听到各种人说,C# 是对 C++ 的改进尝试。我猜这是一个完全不同的问题,但如果一种语言更加复杂,有更多的语法结构(LINQ 呢?),并且与其标准库紧密绑定(从 C++ 转过来,我花了相当长的时间才明白 int[] 是从 IEnumerable<int> 继承而来的类),那么它怎么能成为一种改进呢? - Paulius
1
@onebyone:我认为最好不要重复自己或明确指定默认值,这样可以更清晰地表达。例如,我认为在默认情况下所有方法都是私有的,因此声明一个方法为私有的意义不大。但是,在一些语法必须指定的地方,我会说明。基本上,我试图减少代码中多余的措辞(显然不包括这里的注释),以便我们可以专注于它的功能,而不是编译器想让我说什么。 - Steven Sudit
2
@Dan:是的,谢谢你澄清。当我说我会避免语法时,这些语法编译器可以省略而不会改变行为。显然,像volatile和const之类的东西会影响行为,所以它们不算在内。但override上的虚函数只是调味品,我更喜欢少加调味剂。 - Steven Sudit
显示剩余18条评论

4
添加 virtual 对结果并没有显著影响。我倾向于使用它,但这实际上是一个主观问题。然而,如果您确保在 Visual C++ 中使用 overridesealed 关键字,您将大大提高在编译时捕获错误的能力。
我在我的 PCH 中包含以下行:
#if _MSC_VER >= 1400
#define OVERRIDE override
#define SEALED sealed
#else
#define OVERRIDE
#define SEALED
#endif

很好 - 我不知道 MSVC 对于本地 C++ 代码有这个功能。我可能会做类似于你的事情(尽管我怀疑团队会否决我 - “那不是 C++!”)。 - Michael Burr
谢谢,我在这里学到了新东西!然而,我很想说,除非我绝对必须使用非标准语言扩展,否则我不想使用它们。似乎 #define 魔法可以使代码跨平台编译,但当你在 Windows 以外的代码库上工作时,它可能无法限制错误的产生。因此,我不会投赞成或反对票。 - nonsensickle
1
你现在可以(至少在理论上)摆脱你的 #define 魔法了。override现在是标准的(C++11),你可以用final代替sealed(同样是C++11)。 - Jeremy Sorensen
1
或者如果您仍然需要使用旧版本的Visual Studio,您可以将其重写,使其默认将SEALED定义为final,但对于这些旧版本的VS,则将其设置为sealed - Justin Time - Reinstate Monica

0
我能想到一个缺点: 当一个类成员函数没有被覆盖并且你声明它为虚函数时,你会在该类定义的虚表中添加一个不必要的条目。

如果基本方法已被覆盖,那么你已经承担了这个惩罚。将覆盖的函数标记为虚函数并没有增加任何东西,它只是对程序员的一种提示。 - Martin Beckett

0

注意:我的回答涉及到C++03,有些人仍然在使用它。C++11引入了overridefinal关键字,正如@JustinTime在评论中建议的那样,应该使用它们来代替以下建议。

已经有很多答案了,其中两种相反的观点最为突出。我想将@280Z28在他的回答中提到的内容与@StevenSudit的观点以及@Abhay的风格指南结合起来。

我不同意@280Z28的观点,除非你确定只会在Windows上使用该代码,否则不要使用Microsoft的语言扩展。

但我喜欢这些关键字。那么为什么不只是使用一个#define-d关键字添加以增加清晰度呢?

#define OVERRIDE
#define SEALED

或者

#define OVERRIDE virtual
#define SEALED virtual

区别在于您对第三点中所述情况下想要发生什么的决定。

3 - 如果由于某些错误,IFoo中的虚拟被删除,则所有子项仍将正常运行(CFooSpecialization :: DoBar仍将覆盖CFooBase :: DoBar,而不仅仅是隐藏它)。

尽管我认为这是编程错误,因此没有“修复”并且您可能甚至不应该费心减轻它,而应确保它崩溃或以其他方式通知程序员(尽管我现在想不出一种方法)。

如果您选择第一种选项并且不喜欢添加#define,那么您可以使用注释,例如:

/* override */
/* sealed */

这样就可以满足所有需要清晰度的情况,因为我不认为单独使用虚拟这个词汇足够表达您想要的意思。


只需要指出两件事:1)overridesealed在参数列表之后,而不是在开头。 void func() virtual { /* ... */ }不是有效的函数,但是只要它覆盖了基类中的虚函数,void func() override { /* ... */ }就是有效的。 2)从C++11开始,override是ISO C++中的实际说明符,具有与Microsoft特定版本相同的语义,而final也是ISO C++11说明符,具有(据我所知)与Microsoft特定的sealed相同的语义。 - Justin Time - Reinstate Monica
@JustinTime 考虑到这个问题是在2009年提出的,我的回答是针对C++03的,一些人仍在使用。我很清楚C++11确实提供了你提到的关键字,但我认为它还没有适用于所有的C++社区。然而,你的评论指出我可能需要一个前言来解释我的答案。 - nonsensickle
@JustinTime 另外,你在我的回答中哪里看到我建议在参数列表后面加上 virtual 关键字?我真的不明白你的第一点是从哪里来的... - nonsensickle
此外,C++11并没有添加上下文相关的关键字sealed,而是添加了final,据我所知,它与旧版的Microsoft特定的sealed具有相同的语义。 - Justin Time - Reinstate Monica
@JustinTime,你知道有关overridefinal语义方面的详细信息的好链接吗? - green diod
显示剩余5条评论

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