FAQ:Inheritance 篇——What your mother never told you

文章目录

1、How can I set up my class so it won’t be inherited from?(如何设置类,使它不会被继承?)

Just declare the class final.

But also ask yourself why you want to? There are two common answers:

  • For efficiency: to avoid your function calls being virtual.
  • For safety: to ensure that your class is not used as a base class (for example, to be sure that you can copy objects without fear of slicing).

In today’s usual implementations, calling a virtual function entails fetching the “vptr” (i.e. the pointer to the virtual table) from the object, indexing into it via a constant, and calling the function indirectly via the pointer to function found at that location. A regular call is most often a direct call to a literal address. Although a virtual call seems to be a lot more work, the right way to judge costs is in comparison to the work actually carried by the function. If that work is significant, the cost of the call itself is negligible by comparison and often cannot be measured. If, however, the function body is simple (i.e. an accessor or a forward), the cost of a virtual call can be measurable and sometimes significant.

The virtual function call mechanism is typically used only when calling through a pointer or a reference. When calling a function directly for a named object (e.g. one allocated on the stack of the caller), the compiler inserts code for a regular call. Note, however, that frequent such use may indicate other problems with the design - virtual functions work only in tandem with polymorphism and indirect use (pointers and references). Such cases may warrant a design review for overuse of virtual.

只需要将类声明为 final

但也要问问自己为什么要这样做?有两种常见的答案。

  • 为了效率:避免你的函数调用是 virtual 的。
  • 为了安全:确保你的类不会被用作基类(例如,确保你可以复制对象而不用担心切片)。

在如今通常的实现中,调用虚函数需要从对象中获取“vptr”(即指向虚表的指针),通过常量为其建立索引,并通过在该位置找到的函数的指针间接调用该函数。常规调用通常是对字面地址的直接调用。虽然一个虚拟调用看起来工作量很大,但判断成本的正确方法是与功能实际承担的工作量进行比较。如果这项工作很重要,那么调用本身的成本就可以忽略不计,通常无法衡量。然而,如果函数体很简单(即访问器或转发),虚调用的成本是可以衡量的,有时是很重要的。

虚函数调用机制通常仅在通过指针或引用调用时使用。当直接为命名对象(例如,在调用者的堆栈上分配的对象)调用函数时,编译器为常规调用插入代码。但要注意,频繁使用虚函数可能会导致设计上的其他问题——虚函数只能与多态和间接使用(指针和引用)一起使用。这种情况可能需要对过度使用virtual 进行设计审查。

2、How can I set up my member function so it won’t be overridden in a derived class? (如何设置成员函数,使得它不会在派生类中被覆盖?)

Just declare the function final.

But again, ask yourself why you want to. See the reasons under given for final classes.

只需要将函数声明为 final

但是同样的,问问自己为什么想要这样做。请参阅下面给出的 final 类原因。

3、Is it okay for a non-virtual function of the base class to call a virtual function? (基类的非虚函数调用虚函数可以吗?)

Yes. It’s sometimes (not always!) a great idea. For example, suppose all Shape objects have a common algorithm for printing, but this algorithm depends on their area and they all have a potentially different way to compute their area. In this case Shape’s area() member function would necessarily have to be virtual (probably pure virtual) but Shape::print() could, if we were guaranteed no derived class wanted a different algorithm for printing, be a non-virtual defined in the base class Shape.

可以。这有时(并不总是!)是个好主意。例如,假设所有 Shape 对象都有一个通用的打印方法,但这个算法取决于它们的面积,且它们都有可能有不同的方法来计算它们的面积。在这种情况下,Shapearea() 成员函数必须是虚函数(可能是纯虚函数),但如果我们保证没有派生类需要不同的打印算法,Shape::print() 可以在基类 Shape 中定义成非纯虚函数。

#include "Shape.h"
void Shape::print() const
{
   
    float a = this->area();  // area() is pure virtual
    // ...
}

4、That last FAQ confuses me. Is it a different strategy from the other ways to use virtual functions? What’s going on? (上一个FAQ让我很困惑,它和其他使用虚函数的方法有什么不同的吗?)

Yes, it is a different strategy. Yes, there really are two different basic ways to use virtual functions:

  1. Suppose you have the situation described in the previous FAQ: you have a member function whose overall structure is the same for each derived class, but has little pieces that are different in each derived class. So the algorithm is the same, but the primitives are different. In this case you’d write the overall algorithm in the base class as a public member function (that’s sometimes non-virtual), and you’d write the little pieces in the derived classes. The little pieces would be declared in the base class (they’re often protected, they’re often pure virtual, and they’re certainly virtual), and they’d ultimately be defined in each derived class. The most critical question in this situation is whether or not the public member function containing the overall algorithm should be virtual. The answer is to make it virtual if you think that some derived class might need to override it.

  2. Suppose you have the exact opposite situation from the previous FAQ, where you have a member function whose overall structure is different in each derived class, yet it has little pieces that are the same in most (if not all) derived classes. In this case you’d put the overall algorithm in a public virtual that’s ultimately defined in the derived classes, and the little pieces of common code can be written once (to avoid code duplication) and stashed somewhere (anywhere!). A common place to stash the little pieces is in the protected part of the base class, but that’s not necessary and it might not even be best. Just find a place to stash them and you’ll be fine. Note that if you do stash them in the base class, you should normally make them protected, since normally they do things that public users don’t need/want to do. Assuming they’re protected, they probably shouldn’t be virtual: if the derived class doesn’t like the behavior in one of them, it doesn’t have to call that member function.

For emphasis, the above list is a both/and situation, not an either/or situation. In other words, you don’t have to choose between these two strategies on any given class. It’s perfectly normal to have member function f() correspond to strategy #1 while member function g() corresponds to strategy #2. In other words, it’s perfectly normal to have both strategies working in the same class.

是的,这是一种不同的策略。是的,实际上有两种不同的使用虚函数的基本方法:

  1. 假设你有前面FAQ中描述的情况:您有一个成员函数,其总体结构对于每个派生类都是相同的,但是在每个派生类中有一小部分不同。所以算法是相同的,但基元是不同的。在这种情况下,您将在基类中将整个算法作为 public 成员函数(有时是非 virtual 函数)编写,并在派生类中编写小的部分。小的部分会在基类中声明(它们通常是protected,它们通常是纯虚的,它们当然是virtual),它们最终会在每个派生类中定义。在这种情况下,最关键的问题是包含整个算法的 public 成员函数是否应该是虚函数。如果您认为某些派生类可能需要覆盖它,那么答案是将其设置为 virtual

  2. 假设你的情况与前面的问题完全相反:你有一个成员函数,它的整体结构在每个派生类中都是不同的,但它有一小部分在大多数(假设不是全部)派生类中是相同的。这种情况下,你应该把整个算法放到最终在派生类中定义的 public virtual 中,而那一小段的公共代码可以编写一次(避免代码重复)存储在某处(任何地方)。存放小片段的常见地方是基类的 protected 部分,但这并不是必须的,甚至可能不是最好的。只需要找个地方藏起来就可以了。请注意,如果你确实将它们放到基类中,则通常它们应该是 protected 的,因为通常它们执行 public 用户不需要/不想要指向的操作。假设它们是 protected 的,它们可能不应该是虚函数:如果子类不喜欢其中一个成员函数的行为,它不必调用该成员函数。

需要强调的是,上面的列表是一种 both/and 的情况,而不是 either/or 的情况。换句话说,对于任何给定的类,你不必在这两种策略中做出选择。成员函数 f() 对应策略#1,而成员函数 g() 对应策略#2,这是完全正常的。换句话说,在同一个类中同时使用两种策略是完全正常的。

5、Should I use protected virtuals instead of public virtuals? (我应该使用 protected 的虚函数而不是 public 虚函数吗?)

Sometimes yes, sometimes no.
有时候是,有时候不是。

First, stay away from always/never rules, and instead use whichever approach is the best fit for the situation. There are at least two good reasons to use protected virtuals (see below), but just because you are sometimes better off with protected virtuals does not mean you should always use them. Consistency and symmetry are good up to a point, but at the end of the day the most important metrics are cost + schedule + risk, and unless an idea materially improves cost and/or schedule and/or risk, it’s just symmetry for symmetry’s sake (or consistency for consistency’s sake, etc.).

首先,不要使用always/never规则,而是使用最适合情况的方法。至少有两个很好的理由使用protected virtuals(见下文),但仅仅因为有时使用 protected virtuals 更好,并不意味着您应该总是使用它们。一致性和对称性在一定程度上是好的,但归根结底,最重要的指标是成本+进度+风险,除非某个想法能够实质性地改善成本、进度和/或风险,否则它只是为了对称而对称(或为了一致性而一致,等等)。

The cheapest + fastest + lowest risk approach in my experience ends up resulting in most virtuals being public, with protected virtuals being used whenever you have either of these two cases: the situation discussed in the previous FAQ or the situation discussed in relation to the hiding rule.

根据我的经验,最便宜的+最快的+最低风险的方法最终会导致大多数虚函数为 public,当您遇到以下两种情况之一时,就会使用 protected virtuals:在前面的FAQ中讨论的情况或与隐藏规则有关的情况。

The latter deserves some additional commentary. Pretend you have a base class with a set of overloaded virtuals. To make the example easy, pretend there are just two: virtual void f(int) and virtual void f(double). The idea of the Public Overloaded Non-Virtuals Call Protected Non-Overloaded Virtuals idiom is to change the public overloaded member functions to non-virtuals, and make those call protected non-overloaded virtuals.

后者值得进一步说明。假设您有一个基类,其中包含一组重载的虚函数。为了简化这个例子,假设只有两个虚函数:virtual void f(int)virtual void f(double)。『公共重载非虚函数调用受保护的非重载虚函数』的思想是将公共重载成员函数改为非虚函数,并使其调用受保护的非重载虚函数。

Code using public overloaded virtuals:

使用公共重载虚函数的代码:

class Base {
   
public:
	virtual void f(int x);    // May or may not be pure virtual
	virtual void f(double x); // May or may not be pure virtual
};

通过使用Public Overloaded Non-Virtuals Call Protected Non-Overloaded Virtuals习语来改进这一点:

class Base {
   
public:
	void f(int x)    {
    f_int(x); }  // Non-virtual
	void f(double x) {
    f_dbl(x); }  // Non-virtual
protected:
	virtual void f_int(int);
	virtual void f_dbl(double);
};

下面是原始代码的概述:

Member Function Public? Inline? Virtual? Overloaded?
f(int) & f(double) Yes No Yes Yes

下面是使用Public Overloaded Non-Virtuals Call Protected Non-Overloaded Virtuals 改进代码的概
述:

Member Function Public? Inline? Virtual? Overloaded?
f(int) & f(double) Yes Yes No Yes
f_int(int) & f_dbl(double) No No Yes No

The reason I and others use this idiom is to make life easier and less error-prone for the developers of the derived classes. Remember the goals stated above: schedule + cost + risk? Let’s evaluate this Idiom in light of those goals. From a cost/schedule standpoint, the base class (singular) is slightly larger but the derived classes (plural) are slightly smaller, for a net (small) improvement in schedule and cost. The more signicant improvement is in risk: the idiom packs the complexity of properly managing the hiding rule into the base class (singular). This means the derived classes (plural) more-or-less automatically handle the hiding rule, so the various developers who produce those derived classes can remain almost completely focused on the details of the derived classes themselves — they need not concern themselves with the (subtle and often misunderstood) hiding rule. This greatly reduces the chance that the writers of the derived classes will screw up the hiding-rule.

我和其他人使用这个习语的原因是让派生类的开发人员的开发更容易,更不容易出错。还记得上面提到的目标吗:进度+成本+风险?让我们根据这些目标来评估这个习语。从成本/进度的角度来看,基类(单数)稍微大一些,但是派生类(复数)稍微小一些,这是为了进度和成本的净(小)改进。更显著的改进是在风险方面:习惯用法将适当管理隐藏规则的复杂性打包到基类(单数)中。这意味着派生类(复数)或多或少会自动处理隐藏规则,因此生成这些派生类的各种开发人员可以几乎完全专注于派生类本身的细节——他们无需关心(微妙且经常被误解的)隐藏规则。这大大减少了派生类的作者破坏隐藏规则的机会。
With apologies to Mr. Spock, the needs of the many (the derived classes (plural)) outweigh the needs of the one (the base class (singular)).

在向Spock先生道歉的同时,“多类”(派生类(复数))的需求超过了“一类”(基类(单数))的需求。

(Read up on the Hiding Rule for why you need to be careful about overriding some-but-not-all of a set of overloaded member functions, and therefore why the above makes life easier on derived classes.)

(仔细阅读隐藏规则,了解为什么你需要小心覆盖一组重载成员函数的某些但不是全部,因此,上面的内容使派生类的开发更容易。)

6. When should I use private virtuals?(什么时候应该使用私有的虚函数)

When you need to make specific behavior in a base class customizable in derived classes, while protecting the semantics of the interface (and/or the base algorithm therein), which is defined in public member functions that call private virtual member functions.

当您需要在派生类中定制基类中的特定行为,同时保护接口(和/或其中的基算法)的语义时,该语义定义在调用私有虚成员函数的公共成员函数中。

One case where private virtuals show up is when implementing the Template Method design pattern. Some experts, e.g., Herb Sutter’s C/C++ Users Journal article Virtuality, advocate it as a best practice to always define virtual functions private, unless there is a good reason to make them protected. Virtual functions, in their view, should never be public, because they define the class’ interface, which must remain consistent in all derived classes. Protected and private virtuals define the class’ customizable behavior, and there is no need to make them public. A public virtual function would define both interface and a customization point, a duality that could reflect weak design.

在实现模板方法设计模式时,会出现私有虚拟函数。一些专家,例如Herb Sutter的C/ C++用户期刊文章Virtuality,主张将虚函数定义为私有,除非有充分的理由将它们定义为protected。在他们看来,虚函数永远不应该是公共的,因为它们定义了类的接口,在所有派生类中必须保持一致。受保护虚函数和私有虚函数定义了类的可定制行为,没有必要将它们公开。公共虚函数定义接口和自定义点,这种二元性可能反映了设计的薄弱。

By the way, it confuses most novice C++ programmers that private virtuals can be overridden, let alone are valid at all. We were all taught that private members in a base class are not accessible in classes derived from it, which is correct. However this inaccessibility by the derived class does not have anything to do with the virtual call mechanism, which is to the derived class. Since that might confuse novices, the C++ FAQ formerly recommended using protected virtuals rather than private virtuals. However the private virtual approach is now common enough that confusion of novices is less of a concern.

顺便说一下,私有虚函数可以被重写,这让大多数C++新手感到困惑,更不用说它是有效的了。我们都知道基类中的私有成员在派生类中不可访问,这是正确的。但是,派生类的这种不可访问性与虚调用机制没有任何关系,虚调用机制是针对派生类的。因为这可能会让初学者感到困惑,所以C++ FAQ之前建议使用受保护虚函数,而不是私有虚函数。然而,私有虚拟方法现在已经足够常见了,初学者的困惑也就不那么令人担心了。

You might ask, What good is a function that the derived class can’t call? Even though the derived class can’t call it in the base class, the base class can call it which effectively calls down to the (appropriate) derived class. And that’s what the Template Method pattern is all about.

你可能会问,一个派生类不能调用的函数有什么用?尽管派生类不能调用基类中的它,但基类可以调用它,从而有效地向下调用(适当的)派生类。这就是模板方法模式的全部内容。

Think of “Back to the Future.” Assume the base class is written last year, and you are about to create a new derived class later today. The base class’ member functions, which might have been compiled and stuck into a library months ago, will call the private (or protected) virtual, and that will effectively “call into the future” - the code which was compiled months ago will call code that doesn’t even exist yet - code you are about to write in the next few minutes. You can’t access private members of the base class - you can’t reach into the past, but the past can reach into the future and call your member functions which you haven’t even written yet.

想想《回到未来》。假设基类是去年写的,今天晚些时候你要创建一个新的派生类。基类的成员函数,它可能是几个月前被编译并卡在库中的,它会调用私有的(或受保护的)虚函数,这将有效地“调用未来”——几个月前编译的代码将调用甚至还不存在的代码——即将在接下来的几分钟编写的代码。你不能访问基类的私有成员——你不能访问过去,但过去能访问未来,调用你还没有编写的成员函数。

Here is what that Template Method pattern looks like:
模板方法模式如下所示:

class MyBaseClass {
   
public:
	void myOp();
	
private:
	virtual void myOp_step1() = 0;
	virtual void myOp_step2();
};

void MyBaseClass::myOp()
{
   
	// Pre-processing...
	
  	myOp_step1();  // call into the future - call the derived class
  	myOp_step2();  // optionally the future - this one isn't pure virtual
  	
  	// Post-processing...
}

void MyBaseClass::myOp_step2()
{
   
	// this is "default" code - it can optionally be customized by a derived class
}

In this example, public member function MyBaseClass::myOp() implements the interface and basic algorithm to perform some operation. The pre- and post-processing, as well as the sequence of step 1 and step 2, are intentionally fixed and cannot be customized by a derived class. If MyBaseClass::myOp() was virtual, the integrity of that algorithm would be seriously compromised. Instead, customization is restricted to specific “pieces” of the algorithm, implemented in the two private virtual functions. This enforces better compliance of derived classes to the original intent embodied in the base class, and also makes customization easier - the derived class’ author needs to write less code.

在这个例子中,公共成员函数 MyBaseClass::myOp() 实现了执行某些操作的接口和基本算法。预处理和后处理以及步骤1和步骤2的顺序是故意固定的,且不能由派生类自定义。如果 MyBaseClass::myOp() 是虚函数,那么该算法的完整性将受到严重损害。相反,自定义仅限于算法的特定“部分”,在两个私有虚函数中实现。这使得派生类更好地符合基类中体现的原始意图,也使自定义更容易——派生类的作者需要编写更少的代码。

If MyBaseClass::myOp_step2() might need to be called by the derived class, for example, if the derived class might need (or want) to use that code to simplify its own code, then that can be promoted from a private virtual to a protected virtual. If that is not possible because the base class belongs to a different organization, as a band-aid the code can be copied.

如果派生类可能需要调用 MyBaseClass::myOp_step2() ,例如,如果派生类可能需要(或希望)使用该代码来简化自己的代码,那么可以将其从私有虚函数提升为受保护的虚函数。如果因为基类属于不同的组织而无法做到这一点,作为权宜之计,可以复制代码。

(At this point I can almost read your thoughts: “What? Copy code??!? Are you KIDDING??!? That would increase maintenance cost and duplicate bugs!! Are you CRAZY??!?” Whether I’m crazy remains to be seen, but I am experienced enough to realize life sometimes paints you into a corner. If the base class can’t be modified, sometimes the “least bad” of the bad alternatives is to copy some code. Remember, one size does not fit all, and “think” is not a four-letter word. So hold your nose and do whatever is the least bad thing. Then shower. Twice. But if you risk the team’s success because you are waiting for some third party to change their base class, or if you use #define to change the meaning of private, you might have chosen a worse evil. And oh yea, if you copy the code, mark it with a big fat comment so I won’t come along and think you are crazy!! SMILE!.)
On the other hand, if you are creating the base class and if you aren’t sure whether derived class’s might want to call MyBaseClass::myOp_step2(), you can declare it protected just in case. And in that case, you’d better put a big fat comment next to it so Herb doesn’t come along and think you’re crazy! Either way, somebody is going to think you’re crazy.

另一方面,如果你正在创建基类且不确定派生类是否有可能要调用MyBaseClass::myOp_step2(),你可以将其声明为 protected 以防万一。在这种情况下,你最好在旁边加个大大的评论,这样Herb就不会出现,觉得你疯了!不管怎样,都会有人觉得你疯了。

7. When my base class’s constructor calls a virtual function on its this object, why doesn’t my derived class’s override of that virtual function get invoked?(当基类的构造函数在它的this对象上调用虚函数时,为什么派生类的重载虚函数不被调用?)

Because that would be very dangerous, and C++ is protecting you from that danger.

因为这是非常危险的,而C++正在保护你免受这种危险。

The rest of this FAQ gives a rationale for why C++ needs to protect you from that danger, but before we start that, be advised that you can get the effect as if dynamic binding worked on the this object even during a constructor via The Dynamic Binding During Initialization Idiom.

本FAQ的其余部分给出了为什么C++需要保护您免受这种危险的基本原理,但在我们开始之前,建议您可以通过The Dynamic Binding During Initialization Idiom来获得这种效果,就好像动态绑定在 this 对象上,即使在构造函数期间也可以工作一样。

You can call a virtual function in a constructor, but be careful. It may not do what you expect. In a constructor, the virtual call mechanism is disabled because overriding from derived classes hasn’t yet happened. Objects are constructed from the base up, “base before derived”.

可以在构造函数中调用虚函数,但是要小心。它可能不会做你期望的事情。在构造函数中,禁用了虚调用机制,因为派生类的覆盖还没有发生。对象是从基类开始构造的,“先基后派生”。

#include<string>
#include<iostream>
using namespace std;

class B {
   
public:
	B(const string& ss) {
    cout << "B constructor\n"; f(ss); }
    virtual void f(const string&) {
    cout << "B::f\n";}
};

class D : public B {
   
public:
	D(const string & ss) :B(ss) {
    cout << "D constructor\n";}
    void f(const string& ss) {
    cout << "D::f\n"; s = ss; }
private:
    string s;
};

int main()
{
   
	D d("Hello");
}

编译并输出:

B constructor
B::f
D constructor

Note not D::f. Consider what would happen if the rule were different so that D::f() was called from B::B(): Because the constructor D::D() hadn’t yet been run, D::f() would try to assign its argument to an uninitialized string s. The result would most likely be an immediate crash. So fortunately the C++ language doesn’t let this happen: it makes sure any call to this->f() that occurs while control is flowing through B’s constructor will end up invoking B::f(), not the override D::f().

注意,输出中没有 D::f。考虑一下,如果规则不同,在B::B() 中调用D::f() 会发生什么?因为构造函数 D::D() 还没有运行,D::f() 会尝试将它的参数赋值给一个没有初始化的字符串 s。结果很可能是立即崩溃。所以幸运的是,C++语言不允许这种情况发生:它确保控制流流经 B 的构造函数时,发生的任何对 this->f() 的调用最终都调用B::f(),而不是覆盖的 D::f()

Destruction is done “derived class before base class”, so virtual functions behave as in constructors: Only the local definitions are used – and no calls are made to overriding functions to avoid touching the (now destroyed) derived class part of the object.

析构是“先派生后基类”,因此虚函数的行为与构造函数一样:只使用局部定义——并不会调用覆盖函数,以避免触及对象的派生类部分(现在已经被析构)。

For more details see D&E 13.2.4.2 or TC++PL3 15.4.3.

更多相关信息见D&E 13.2.4.2 或 TC++PL3 15.4.3.

It has been suggested that this rule is an implementation artifact. It is not so. In fact, it would be noticeably easier to implement the unsafe rule of calling virtual functions from constructors exactly as from other functions. However, that would imply that no virtual function could be written to rely on invariants(不变量) established by base classes. That would be a terrible mess.

8. Okay, but is there a way to simulate that behavior as if dynamic binding worked on the this object within my base class’s constructor? (好的,但是有没有一种方法来模拟这种行为,就好像动态绑定在基类的构造函数中的this对象上工作一样?)

Yes: the Dynamic Binding During Initialization idiom (AKA Calling Virtuals During Initialization).

当然:初始化期间动态绑定的惯用法(即在初始化期间调用虚函数)

To clarify, we’re talking about the situation when Base’s constructor calls virtual functions on its this object:

澄清一点,我们讨论的是 Base 构造函数在其 this 对象上调用虚函数的情况:

class Base {
   
public:
	Base();
  	// ...
  	virtual void foo(int n) const; // often pure virtual
  	virtual double bar() const;    // often pure virtual
  	// if you don't want outsiders calling these, make them protected
};

Base::Base()
{
   
	// ...
  	foo(42);  // Warning: does NOT dynamically bind to the derived class
  	bar();    // (ditto)
  	// ...
}

class Derived : public Base {
   
public:
	// ...
  	virtual void foo(int n) const;
  	virtual double bar() const;
};

This FAQ shows some ways to simulate dynamic binding as if the calls made in Base’s constructor dynamically bound to the this object’s derived class. The ways we’ll show have tradeoffs, so choose the one that best fits your needs, or make up another.

这个 FAQ 展示了一些模拟动态绑定的方法,就好像在 Base 的构造函数中的调用,动态绑定到 this对象的派生类。我们展示的方法是有权衡的,所以选择最适合您需求的方法或者选择另一个。

The first approach is a two-phase initialization. In Phase I, someone calls the actual constructor; in Phase II, someone calls an “init” function on the object. Dynamic binding on the this object works fine during Phase II, and Phase II is conceptually part of construction, so we simply move some code from the original Base::Base() into Base::init().

第一种方法是有两个阶段的初始化。第一阶段,调用实际的构造函数;第二阶段,在对象上调用 init 函数。第二阶段中 this 对象上的动态绑定是可以正常工作的,且第二阶段从概念上讲是构造的一部分,因此我们只需将一些代码从原始的 Base::Base() 移动到 Base::init() 中。

class Base {
   
public:
	void init();  // may or may not be virtual
  	// ...
  	virtual void foo(int n) const; // often pure virtual
  	virtual double bar() const;    // often pure virtual
};

void Base::init()
{
   
	// Almost identical to the body of the original Base::Base()
  	// ...
  	foo(42);
  	bar();
  	// ...
}

class Derived : public Base {
   
public:
	// ...
  	virtual void foo(int n) const;
  	virtual double bar() const;
};

The only remaining issues are determining where to call Phase I and where to call Phase II. There are many variations on where these calls can live; we will consider two.

剩下的问题是决定在哪里调用第一阶段和在哪里调用第二阶段。可以进行这些调用的变体有很多个,这里我们考虑两个。

The first variation is simplest initially, though the code that actually wants to create objects requires a tiny bit of programmer self-discipline, which in practice means you’re doomed. Seriously, if there are only one or two places that actually create objects of this hierarchy, the programmer self-discipline is quite localized and shouldn’t cause problems.

第一种变化最初是最简单的,尽管实际想要创建对象的代码需要程序员有一点点自律,这在实践中意味着你注定要失败。严肃地说,如果只有一两个地方真正创建了这种层次结构的对象,那么程序员的自律是相当局限的,不应该造成问题。

In this variation, the code that is creating the object explicitly executes both phases. When executing Phase I, the code creating the object either knows the object’s exact class (e.g., new Derived() or perhaps a local Derived object), or doesn’t know the object’s exact class (e.g., the virtual constructor idiom or some other factory). The “doesn’t know” case is strongly preferred when you want to make it easy to plug-in new derived classes.

在这个变体中,创建对象的代码显式地执行了这两个阶段。当执行第一阶段时,创建对象的代码要么知道对象的确切类(例如,new Derived()或可能是一个局部的派生对象),要么不知道对象的确切类(例如,虚构造函数习语或其他工厂)。当你想要更容易地插入新的派生类时,“不知道”的情况是强烈的首选。

Note: Phase I often, but not always, allocates the object from the heap. When it does, you should store the pointer in some sort of managed pointer, such as a std::unique_ptr, a reference counted pointer, or some other object whose destructor deletes the allocation. This is the best way to prevent memory leaks when Phase II might throw exceptions. The following example assumes Phase I allocates the object from the heap.

注意:阶段I 经常从堆中分配对象。当它发生时,你应该将指针存储在某种托管指针中,例如,std::unique_ptr、引用计数指针或其他对象中,该对象de 析构函数会删除分配。当第二阶段可能抛出异常时,这是防止内存泄漏的最好方法。下面的例子假设阶段I从堆中分配对象。

#include <memory>
void joe_user()
{
   
	std::unique_ptr<Base> p( /*...somehow create a Derived object via new...*/ );
  	p->init();
  	// ...
}

The second variation is to combine the first two lines of the joe_user function into some create function. That’s almost always the right thing to do when there are lots of joe_userlike functions. For example, if you’re using some kind of factory, such as a registry and the virtual constructor idiom, you could move those two lines into a static member function called Base::create():

第二种变体是将 joe_user 函数的前两行合并到 create 函数中。在有很多类似 joe_user 的函数时,这样做几乎总是正确的。例如,如果你正在使用某种工厂函数,比如注册表和虚构造函数,可以将这两行代码移到一个名为 Base::create() 的静态成员函数中:

#include <memory>
class Base {
   
public:
	// ...
  	using Ptr = std::unique_ptr<Base>;  // type aliases simplify the code
  	static Ptr create();
  	// ...
};

Base::Ptr Base::create()
{
   
	Ptr p( /*...use a factory to create a Derived object via new...*/ );
  	p->init();
  	return p;
}

This simplifies all the joe_user-like functions (a little), but more importantly, it reduces the chance that any of them will create a Derived object without also calling init() on it.

这简化了所有类似 joe_user 的函数(有点),但更重要的是,它减少了在不调用 init() 的情况下创建派生对象的可能性。

void joe_user()
{
   
	Base::Ptr p = Base::create();
  	// ...
}

If you’re sufficiently clever and motivated, you can even eliminate the chance that someone could create a Derived object without also calling init() on it. An important step in achieving that goal is to make Derived’s constructors, including its copy constructor, protected or private..

如果你足够聪明和积极,你甚至可以消除别人在创建派生对象时不调用 init() 的可能性。实现这一目标的重要一步是将 Derived 的构造函数(包括复制构造函数)设置为 protectedprivate

The next approach does not rely on a two-phase initialization, instead using a second hierarchy whose only job is to house member functions foo() and bar(). This approach doesn’t always work, and in particular it doesn’t work in cases when foo() and bar() need to access the instance data declared in Derived, but it is conceptually quite simple and clean and is commonly used.

下一种方法不依赖两阶段初始化,而是使用第二个层次结构,其唯一的工作是容纳成员函数 foo()bar()。这种方法并不总是有效,特别是当 foo()bar() 需要访问在 Derived 中声明的实例数据时,它不起作用,但它在概念上非常简单和干净,是常用的。

Let’s call the base class of this second hierarchy Helper, and its derived classes Helper1, Helper2, etc. The first step is to move foo() and bar() into this second hierarchy:

让我们将第二个层次结构的基类称为Helper,以及它的派生类 Helper1Helper2等。第一步是将 foo()bar() 移动到第二个层次结构中:

class Helper {
   
public:
	virtual void foo(int n) const = 0;
  	virtual double bar() const = 0;
};

class Helper1 : public Helper {
   
public:
	virtual void foo(int n) const;
  	virtual double bar() const;
};

class Helper2 : public Helper {
   
public:
	virtual void foo(int n) const;
  	virtual double bar() const;
};

Next, remove init() from Base (since we’re no longer using the two-phase approach), remove foo() and bar() from Base and Derived (foo() and bar() are now in the Helper hierarchy), and change the signature of Base’s constructor so it takes a Helper by reference:

接下来,从基类中删除 init() (因为我们不再使用两阶段方法),从基类和派生类中删除 foo()bar() ( foo()bar() 现在在辅助函数层次结构中),并更改基类构造函数的签名,使其通过引用接收一个辅助函数:

class Base {
   
public:
	Base(const Helper& h);
  	// Remove init() since not using two-phase this time
  	// Remove foo() and bar() since they're in Helper
};

class Derived : public Base {
   
public:
	// Remove foo() and bar() since they're in Helper
};

We then define Base::Base(const Helper&) so it calls h.foo(42) and h.bar() in exactly those places that init() used to call this->foo(42) and this->bar():

然后我们定义 Base::Base(const Helper&),因此它会在 init() 曾经调用 this->foo(42)this->bar() 的地方调用 h.foo(42)h.bar():

Base::Base(const Helper& h)
{
   
	// Almost identical to the body of the original Base::Base()
  	// except for the insertion of h.
  	// ...
  	h.foo(42);
  	h.bar();
  	↑↑ // The h. occurrences are new
  	// ...
}

Finally we change Derived’s constructor to pass a (perhaps temporary) object of an appropriate Helper derived class to Base’s constructor (using the init list syntax). For example, Derived would pass an instance of Helper2 if it happened to contain the behaviors that Derived wanted for functions foo() and bar():

最后,我们修改 Derived 的构造函数,将一个适当的辅助派生类的(可能是临时的)对象传递给 Base 的构造函数(使用 init list语法)。例如,派生类会传递一个 Helper2 的实例,如果它恰好包含派生类需要的 foo()bar() 函数的行为:

Derived::Derived()
	: Base(Helper2())   // ← the magic happens here
{
   
	// ...
}

Note that Derived can pass values into the Helper derived class’s constructor, but it must not pass any data members that actually live inside the this object. While we’re at it, let’s explicitly say that Helper::foo() and Helper::bar() must not access data members of the this object, particularly data members declared in Derived. (Think about when those data members are initialized and you’ll see why.)

请注意,派生类可以将值传递给 Helper 派生类的构造函数,但不能传递实际存在于 this 对象中的任何数据成员。在这里,我们明确声明 Helper::foo()Helper::bar() 不能访问 this 对象的数据成员,特别是在 Derived 中声明的数据成员。(想想这些数据成员是什么时候初始化的,你就知道为什么了。)

Of course the choice of which Helper derived class could be made out in the joe_user-like function, in which case it would be passed into the Derived ctor and then up to the Base ctor:

当然,可以在类似 joe_user 的函数中选择哪个 Helper 派生类,在这种情况下,它将被传递给派生类,然后再传递给基类:

Derived::Derived(const Helper& h)
	: Base(h)
{
   
	// ...
}

If the Helper objects don’t need to hold any data, that is, if each is merely a collection of its member functions, then you can simply pass static member functions instead. This might be simpler since it entirely eliminates the Helper hierarchy.

如果 Helper 对象不需要保存任何数据,也就是说,如果每个 Helper 对象只是其成员函数的集合,那么可以简单地传递静态成员函数。这可能会更简单,因为它完全消除了 Helper 层次结构。

class Base {
   
public:
	using FooFn = void (*)(int);  // type aliases simplify
  	using BarFn = double (*)();   //    the rest of the code
  	Base(FooFn foo, BarFn bar);
  	// ...
};

Base::Base(FooFn foo, BarFn bar)
{
   
	// Almost identical to the body of the original Base::Base()
  	// except the calls are made via function pointers.
  	
  	// ...
  	foo(42);
  	bar();
  	// ...
}

The Derived class is also easy to implement:

class Derived : public Base {
   
public:
	Derived();
  	static void foo(int n); // the static is important!
  	static double bar();    // the static is important!
  	// ...
};

Derived::Derived()
	: Base(foo, bar)  // ← pass the function-ptrs into Base's ctor
{
   
	// ...
}

As before, the functionality for foo() and/or bar() can be passed in from the joe_user-like functions. In that case, Derived’s ctor just accepts them and passes them up into Base’s ctor:

Derived::Derived(FooFn foo, BarFn bar)
  : Base(foo, bar)
{
   
	// ...
}

A final approach is to use templates to “pass” the functionality into the derived classes. This is similar to the case where the joe_user-like functions choose the initializer-function or the Helper derived class, but instead of using function pointers or dynamic binding, it wires the code into the classes via templates.

最后一种方法是使用模板将功能“传递”到派生类中。这类似于 joe_user 类函数选择初始化函数或Helper 派生类的情况,但它没有使用函数指针或动态绑定,而是通过模板将代码连接到类中。

9、I’m getting the same thing with destructors: calling a virtual on my this object from my base class’s destructor ends up ignoring the override in the derived class; what’s going on?(同样的问题也发生在析构函数中:从基类的析构函数中调用this对象的虚函数最终会忽略派生类中的重写; 这是怎么的呢?)

C++ is protecting you from yourself. What you are trying to do is very dangerous, and if the compiler did what you wanted, you’d be in worse shape.

C++保护你不受自己的伤害。你试图做的事情是非常危险的,如果编译器按你想要的做了,情况会更糟。

For rationale of why C++ needs to protect you from that danger, make sure you understand what happens when a constructor calls virtuals on its this object. The situation during a destructor is analogous to that during the constructor. In particular, within the {body} of Base::~Base(), an object that was originally of type Derived has already been demoted (devolved, if you will) to an object of type Base. If you call a virtual function that has been overridden in class Derived, the call will resolve to Base::virt(), not to the override Derived::virt(). Same goes for using typeid on the this object: the this object really has been demoted to type Base; it is no longer an object of type Derived.

要理解为什么C++需要保护你避免这种危险,请确保你理解当构造函数在其 this 对象上调用虚函数时会发生什么。析构函数期间的情况与构造函数期间类似。特别地,在 Base::~Base() 的 {body} 中,一个原本是 Derived 类型的对象已经降级(如果你愿意的话,可以转换为 Base 类型的对象)。如果调用在派生类中被重写的虚函数,则调用将解析为 Base::virt(),而不是覆盖的Derived::virt()。在 this 对象上使用 typeid 也是如此:this 对象实际上已经降级为 Base 类型;它不再是类型为 Derived 的对象。

Reminder to also read this.

提醒你也要阅读这个。

10、Should a derived class redefine (“override”) a member function that is non-virtual in a base class? (派生类应该重定义(覆盖)基类中非虚的成员函数吗?)

It’s legal, but it ain’t moral.

合法但不道德。

Experienced C++ programmers will sometimes redefine a non-virtual function for efficiency (e.g., if the derived class implementation can make better use of the derived class’s resources) or to get around the hiding rule. However the client-visible effects must be identical, since non-virtual functions are dispatched based on the static type of the pointer/reference rather than the dynamic type of the pointed-to/referenced object.

有经验的C++程序员有时为了提高效率(如派生类的实现可以更好地利用派生类的资源)或绕开隐藏的规则而重新定义非虚函数。然而,客户端可见的效果必须是相同的,因为非虚函数是根据指针/引用的静态类型而不是所指向/引用的对象的动态类型分发的。

11、What’s the meaning of, Warning: Derived::f(char) hides Base::f(double)?

It means you’re going to die.

这意味着你要死了。

Here’s the mess you’re in: if Base declares a member function f(double x), and Derived declares a member function f(char c) (same name but different parameter types and/or constness), then the Base f(double x) is “hidden” rather than “overloaded” or “overridden” (even if the Base f(double x) is virtual).

你遇到的麻烦是:如果 Base 声明了一个成员函数 f(double x)Derived 声明了一个成员函数f(char c)(函数名相同,但参数类型和/或常量不同),那么Base f(double x) 是“隐藏的”而不是“重载的”或“覆盖的”(即便基函数 f(double x) 是虚函数)。

class Base {
   
public:
	void f(double x);  // Doesn't matter whether or not this is virtual
};

class Derived : public Base {
   
public:
	void f(char c);  // Doesn't matter whether or not this is virtual
};

int main()
{
   
	Derived* d = new Derived();
  	Base* b = d;
  	b->f(65.3);  // Okay: passes 65.3 to f(double x)
  	d->f(65.3);  // Bizarre: converts 65.3 to a char ('A' if ASCII) and passes it to f(char c); does NOT call f(double x)!!
  	delete d;
  	return 0;
}

Here’s how you get out of the mess: Derived must have a using declaration of the hidden member function. For example,

以下是如何摆脱混乱的方法:Derived必须有隐藏成员函数的 using 声明。例如,

class Base {
   
public:
	void f(double x);
};

class Derived : public Base {
   
public:
	using Base::f;  // This un-hides Base::f(double x)
	void f(char c);
};

If the using syntax isn’t supported by your compiler, redefine the hidden Base member function(s), even if they are non-virtual. Normally this re-definition merely calls the hidden Base member function using the :: syntax. E.g.,

如果编译器不支持using语法,请重新定义隐藏的基类成员函数,即使它们是非虚函数。通常情况下,这个重定义只是使用::语法调用隐藏的基类成员函数。例如,

class Derived : public Base {
   
public:
	void f(double x) {
    Base::f(x); }  // The redefinition merely calls Base::f(double x)
	void f(char c);
};

Note: the hiding problem also occurs if class Base declares a member function f(char).

注意:如果Base 类声明了一个成员函数 f(char),则隐藏问题也会发生。

Note: warnings are not part of the standard, so your compiler may or may not give the above warning.

注意:警告不是标准的一部分,所以你的编译器可能会也可能不会给出上述警告。

Note: nothing gets hidden when you have a base-pointer. Think about it: what a derived class does or does not do is irrelevant when the compiler is dealing with a base-pointer. The compiler might not even know that the particular derived class exists. Even if it knows of the existence of some particular derived class, it cannot assume that a specific base-pointer necessarily points at an object of that particular derived class. Hiding takes place when you have a derived pointer, not when you have a base pointer.

注意:当你有一个基指针的时候,什么都不会被隐藏。想想看:当编译器处理基指针时,派生类做什么或不做什么是无关紧要的。编译器甚至可能不知道特定的派生类的存在。即使它知道某个特定的派生类的存在,它也不能假定某个特定的基指针一定指向那个特定的派生类的对象。隐藏发生在有派生指针时,而不是在有基指针时。

12、Why doesn’t overloading work for derived classes?(为什么派生类的重载不起作用?)

That question (in many variations) are usually prompted by an example like this:

这个问题(在许多变体中)通常由下面这样的例子引发:

#include<iostream>
using namespace std;

class B {
   
public:
	int f(int i) {
    cout << "f(int): "; return i+1; }
	// ...
};

class D : public B {
   
public:
	double f(double d) {
    cout << "f(double): "; return d+1.3; }
	// ...
};

int main()
{
   
	D* pd = new D;
	cout << pd->f(2) << '\n';
	cout << pd->f(2.3) << '\n';
	delete pd;
}

which will produce:

f(double): 3.3
f(double): 3.6

rather than the

f(int): 3
f(double): 3.6

that some people (wrongly) guessed. 有些人(错误地)猜到了。

In other words, there is no overload resolution between D and B. Overload resolution conceptually happens in one scope at a time: The compiler looks into the scope of D, finds the single function double f(double), and calls it. Because it found a match, it never bothers looking further into the (enclosing) scope of B. In C++, there is no overloading across scopes – derived class scopes are not an exception to this general rule. (See D&E or TC++PL4 for details).

换句话说,在 DB 之间没有重载解析。重载解析在概念上一次只发生在一个作用域中:编译器查看 D 的作用域,找到单个函数 double f(double),并调用它。因为它找到了一个匹配项,所以它从不费神去进一步查看 B 的(外部)作用域。在C++中,没有跨作用域的重载——派生类作用域也不是这个通用规则的例外。(详见D&E或TC++PL4)。

But what if I want to create an overload set of all my f() functions from my base and derived class? That’s easily done using a using-declaration, which asks to bring the functions into the scope:

但是如果我想从我的基类和派生类中创建所有 f() 函数的重载集呢? 使用 using-声明可以很容易地做到这一点,它要求将函数引入作用域:

class D : public B {
   
public:
	using B::f; // make every f from B available
	double f(double d) {
    cout << "f(double): "; return d+1.3; }
    // ...
};

Given that modification, the output will be:

f(int): 3
f(double): 3.6

That is, overload resolution was applied to B’s f() and D’s f() to select the most appropriate f() to call.

也就是说,对 Bf()Df() 进行了重载解析,以选择最合适的 f() 进行调用。

13、What does it mean that the “virtual table” is an unresolved external? (“虚表”是一个未解决的外部表是什么意思?)

If you get a link error of the form “Error: Unresolved or undefined symbols detected: virtual table for class Fred,” you probably have an undefined virtual member function in class Fred.

如果出现“Error: Unresolved or undefined symbols detected: virtual table for class Fred”这样的链接错误,说明 Fred 类的虚成员函数未定义。

The compiler typically creates a magical data structure called the “virtual table” for classes that have virtual functions (this is how it handles dynamic binding). Normally you don’t have to know about it at all. But if you forget to define a virtual function for class Fred, you will sometimes get this linker error.

编译器通常为具有虚函数的类创建一个称为“虚表”的神奇数据结构(这就是它处理动态绑定的方式)。通常你根本不需要知道。但是如果忘记为 Fred 类定义虚函数,有时会出现这种链接错误。

Here’s the nitty gritty: Many compilers put this magical “virtual table” in the compilation unit that defines the first non-inline virtual function in the class. Thus if the first non-inline virtual function in Fred is wilma(), the compiler will put Fred’s virtual table in the same compilation unit where it sees Fred::wilma(). Unfortunately if you accidentally forget to define Fred::wilma(), rather than getting a Fred::wilma() is undefined, you may get a “Fred’s virtual table is undefined”. Sad but true.

事实是这样的:许多编译器将这个神奇的“虚表”放在编译单元中,该编译单元定义了类中的第一个非内联虚函数。因此,如果 Fred 中的第一个非内联虚函数是 wilma(),编译器将把 Fred 的虚表放在它看到 Fred::wilma() 的同一个编译单元中。不幸的是,如果你不小心忘记定义 Fred::wilma(),得到的不是Fred::wilma() 是 undefined,而是 “Fred的虚拟表是undefined”。令人难过但却是事实。

相关推荐

  1. Redis<span style='color:red;'>篇</span>

    Redis

    2023-12-13 14:20:04      36 阅读
  2. 模 块

    2023-12-13 14:20:04       15 阅读
  3. 函 数

    2023-12-13 14:20:04       17 阅读
  4. MySql<span style='color:red;'>篇</span>

    MySql

    2023-12-13 14:20:04      11 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-13 14:20:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-13 14:20:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-13 14:20:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-13 14:20:04       20 阅读

热门阅读

  1. 网络协议的深入了解!

    2023-12-13 14:20:04       35 阅读
  2. 处理器中断的处理

    2023-12-13 14:20:04       32 阅读
  3. fmt用法

    2023-12-13 14:20:04       39 阅读
  4. [json]定义、读写

    2023-12-13 14:20:04       35 阅读
  5. 从零学算法49

    2023-12-13 14:20:04       38 阅读
  6. ssh无密码自动登录实现原理

    2023-12-13 14:20:04       38 阅读
  7. 移除元素

    2023-12-13 14:20:04       31 阅读
  8. 【经典算法】随机森林

    2023-12-13 14:20:04       37 阅读
  9. uniapp微信小程序点击保存图片

    2023-12-13 14:20:04       46 阅读