精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
One problem with the bare C++ approach is that part of the implementation details of an object still have to be included in the declaration of the class: The client "sees" all the private member (arrays and so forth) even though the compiler will not allow a derived class to access them. 在使用无包装C++方法时,对象的部分实现细节依然包含在类的声明里:客户端“看到”所有的私有成员(数组等等),尽管编译器不允许派生类访问它们,这种能“看到”是个问题。
For the monolithic case, where everything is compiled from the complete source code, this approach is probably still acceptable, although not necessarily desirable.在整体融入情况下,编译器编译了全部代码,这个方法尽管不是最合意的,依然有可能被接受。
For binary (public) distribution of an object, revealing the implementation details in a header file is probably still too much. The client (specifically, the compiler, when compiling the client code) needs to know, at a minimum, the size of the data members when doing memory allocation for instances of the object, even if it never accesses any of the data members. When the object implementer changes the size of the data members, all clients must recompile. Our approach of having the object do the allocation lets you get away with not recompiling, because all the client code ever needs from the object is the addresses of its member functions. But I would definitely not feel comfortable about having a mission-critical system work with an internal discrepancy such as this.对于二进制(公开)发布的对象,在头文件里揭露实现细节依然太过了。客户端(具体指编译器当编译客户端代码时)至少需要知道数据成员的大小,来实现对象实例化,尽管它有可能永远也不会存取数据成员。当对象实现修改了数据成员的大小,所有客户端必须重新编译。因为所有客户端代码永远只需要知道它成员函数的地址,所以我们的目标是让对象进行分配,让你不再重新编译达到更易用。但是我不会明确地觉得满意,因为这类情况下对于关键事务系统工作会带有内部差异性。
As stated above, the only information the client needs from the object is the address of the member functions. Therefore, we could set up a table of pointers to all the (exported) member functions of the object. Then, when invoking the function, the client would simply look up the nth member function's address and jump to it.如上所述,客户端需要知道对象相关的函数。这样,我们能设置一个指针表指向所有(导出)成员函数。那么,当激活函数时,客户端只是简单地查找第n个成员函数的地址且跳转到它上面。
This approach sounds a little complicated to do in C++: We would have to set up a "wrapper class" to replicate the functions within the client code and, within each function, look up and call indirectly the corresponding member function in the object. Next, it would be convenient to have a tool that generates these trivial but tedious-to-implement wrapper functions.这个方法相比C++的方法略微复杂些:我们必须设置个“封装类”来在客户端代码里复制函数,在每个函数里,查找且非直接调用对象里对应函数。随后会讨论,有个工具来生成这些琐碎但是冗长实现的封装函数会方便些。
Moving back from the binary world of member function addresses, let's get back to the higher levels of C++. There is a feature in C++ that provides a similar functionality: the abstract base class. 回到成员函数地址的二进制世界里,让我们回溯到C++高层级里。在C++里提供了类似的功能:抽象基类。
The term abstract base class sounds formidable, but actually it is nothing but a class that declares the syntax of one or more of its functions without implementing them. This class cannot be instantiated—it merely serves as a base class for other classes that fill in the missing "link." To be a "real" C++ class that can be instantiated, the deriving class must implement all the "abstract" functions with the exact same syntax.术语抽象基类听起来可怕,但是实际上没什么,它只是一个类,声明但没有实现它的函数而已。这个类不能实例化—它只是服务于派生功能,让别的类来“填空”。对于一个“真实”的C++类来说,它能实例化,派生的类也必须以同样的语法实现所有“抽象的”函数。
There is another advantage to abstract base classes, besides letting you force derived classes to implement an exact function signature: They also let you use the unimplemented function from code in the base class and have C++ choose the actual code at run time.抽象基类的另外一个优点是,他们同样让你在基类代码里使用没实现的函数,让C++在运行时选择实际的运行函数。
You can provide operations in the base class that use other operations (the missing functions) to provide higher-level functionality—essentially, an abstract algorithm with replaceable elementary operations. You could implement a sorting algorithm as an abstract base class, one that uses comparisons and swaps as elementary operations. A derived class can then fill in these operations and the same algorithm can sort completely different objects.你能提供在基类里的操作,基类使用其它操作(遗失的函数)来提供高层级的功能—本质上来说,抽象规则是可替换的元素操作。你能够实现一个排序规则来做为一个基类,另外的程序员可以使用比较和交换操作来做为元素级操作。派生类能够填充这些操作和同样的规则,且同样的算法能够对不同对象完整地排序。
If two classes derive from the same abstract base class, they can share the same abstract operations defined in the base class, and yet they can provide very different functionality by providing different elementary operations. The code in the base class calls functions depending on the class of the actual instance of the object it operates on.如果2个类派生自同样一个抽象基类,他们能够共享基类里同样的抽象操作,且他们能够通过提供不同的元素操作来提供非常不同的功能。基类里的操作调用的函数依赖于对象的实例类型。
A side effect of this architecture is that you can cast a pointer to an instance of a class derived from an abstract base class back to the base class, and then you can use this pointer to invoke functions in the derived class.这个架构的副作用是你能转换派生类指针为基类类型指针,且能用这个指针来激活派生类里的函数。
If multiple classes derive from one abstract base class, a client can invoke the same function on two different objects and C++ will find—at run time—the correct function in the correct derived class.如果从抽象基类里派生了多个类,客户端能激活不同对象上的同名函数,C++在运行时会从派生类里查找对应正确的函数。
How does the compiler accomplish this magic? All the functions in an abstract base class must be virtual functions: You can call a virtual function on a pointer to a base class, and the compiler will call the correct implementation depending on the actual object referred to by the pointer.编译器怎样实现这个魔法呢?所有在抽象基类里的函数必须是虚函数:你能通过基类指针来调用虚函数,编译器会依赖指针指向实际对象的类型来调用正确的实现函数。
How does the compiler implement virtual functions? The compiler builds a table for each class that contains pointers to all the virtual functions declared in the class, whether they are declared (and possibly implemented) in a base class or declared in the derived class. The table is filled in "order of declaration"; that is, if there is no base class with virtual functions, the first declared member function in the derived class (source code order) corresponds to the first entry in the virtual function table for this class. If there are virtual functions declared in base classes, they go before the newly defined virtual functions. The virtual function table is also called the vtable of the class.编译器是怎样实现虚函数的呢?编译器会为每个类构造出表,这个表里包含所有虚函数的指针,不管虚函数是在基类或派生类里声明(可能被实现了)。表以“声明顺序”来填充;也就是说,如果基类里不带虚函数,在派生类里第一个声明的虚函数(源代码顺序)对应虚函数表的第一个入口。如果在基类里有虚函数,他们在派生类里新定义虚函数入口前面。虚函数表同样称为类的vtable。
Note the following: 注意如下情况:
When I described the problem of changing implementations of objects and I stated that the client needs only the addresses of the object's member functions, I proposed a tool to generate wrappers for tables with the exported functions of an object.当我描述修改对象实现问题时,我指出客户端只需要知道对象成员函数的地址,我展示了工具来生成对表的封装,表里有导出函数。
Well, here it is: your C++ compiler. As we saw above, virtual functions are implemented exactly that way: The compiler sets up a table with the addresses of the function (vtable) and lets you wrap these functions with an abstract base class that simply maps calls to members through the table. As you will see, we will use abstract base classes in our samples that provide no actual code—all the members will be pure virtual functions, and all they will do is let you invoke functions in any derived class, once you have cast a pointer to the base class. For the purposes of this article, I call this kind of abstract base class a pure abstract base class; COM calls it an interface.虚函数恰恰实现了我们的要求。我们会使用抽象基类,该类不提供实际的代码—所有成员是纯虚函数,他所能做的只是让你能够激活派生类里的虚函数。结合本文的目的,我称呼这些抽象基类为纯抽象基类;COM称呼它为接口。
Let's use this idea, to begin with, in a subdirectory of the DB sample that has everything compiled into one executable file. Even in this situation, using abstract base classes can be advantageous. You can make sure that you will not accidentally use implementation data that for technical reasons you had to make "public" for C++.现在DB例子里,所有代码编译到一个可执行文件里。在这样的情况下,使用抽象基类也有巨大的优越性。你能确保不会偶然使用实现的数据,这些数据在技术原因的压力下要求你让它成为“公有”的。
Use the DB_CPPDLL sample as the basis. The next sample will be very similar to this one, so if you are short of time, you can skip this one. Its primary purpose is to illustrate that using abstract base classes as function table wrappers is a standard C++ feature, which can also be very useful in a non-component world. 使用DB_CPPDLL类做为基础。下一步的例子会和这个非常象,所以用时短,能忽略此步。它的主要目的是演示使用虚基类做为函数表封装者,这是个标准的C++特点,这在非组件环境下是经常用到。
The resulting header file Interface\dbsrv.h: Interface\dbsrv.h文件里结果形式如下:
class IDB { // Interfaces public: // Interface for data access virtual HRESULT Read(short nTable, short nRow, LPTSTR lpszData) =0; virtual HRESULT Write(short nTable, short nRow, LPCTSTR lpszData) =0; (...) }; HRESULT CreateDB(IDB** ppObj);
The new header "Object\dbsrvimp.h":
#include "..\Interface\dbsrv.h" typedef long HRESULT; class CDB : public IDB { // Interfaces public: // Interface for data access HRESULT Read(short nTable, short nRow, LPTSTR lpszData); (...) };
The resulting DBSRV.CPP (no changes, just adding the instantiation function):DBSRV.CPP里有代码为(没修改,只是添加了实例化函数):
(...) HRESULT CreateDB(IDB** ppObj){ *ppObj=(IDB*) new CDB; // Cast to abstract base class. return NO_ERROR; }
Everything works just as before. If we look at the binary level, there is a slight overhead cost for the indirect function call (get vtable pointer from instance data, get function pointer from vtable, jump), but the performance measured with "Read Multiple" is not significantly affected. The actual "work" performed in the member functions takes orders of magnitude longer than these two additional memory accesses.如果我们从二进制这一层检查一下,这种非直接的函数调用有轻微的间接成本(从实例数据里获取vtable指针,从vtable里获取函数,跳转),但是在“读多个”情况下,性能不会有致命影响。在成员函数里执行的实际“工作”和2个步骤的额外内在存取工作,效率不在一个数量级上。