原文链接:http://www.drdobbs.com/cpp/standard-c-programming-virtual-functions/184403747

By Josée Lajoie and Stanley Lippman, September 01, 2000

As we gain mastery of C++, it is natural to question the rules of thumb that helped us get by in the beginning. Lippman and Lajoie provide an excellent example of such a rule regarding virtual functions, and why that rule should be carefully reconsidered.


[This is the last installment of a column that was being published in C++ Report magazine. Since the magazine ceased publication before this installment could be published, Josée Lajoie and Stan Lippman were gracious enough to let us publish it on the CUJ website. — mb]

At one time, a common question that we used to hear when speaking on C++ was, "Should a virtual function really be declared inline?" These days, we hardly ever hear that question. Rather, what we hear now is "You shouldn't have made print inline. It's wrong to declare a virtual function inline."

The two primary reasons for taking this position are (1) virtual functions are resolved at run-time while the inline facility is a compile-time mechanism, so there is nothing to be gained by the declaration; and (2) declaring a virtual function inline results in multiple copies of the function being defined within our executable, so we pay a space penalty for a function that can't be inlined in any case. An obvious no-brainer.

Only that's not really true. Let's take item (1) first: there are many cases in which a virtual function is resolved statically — essentially any time a derived class virtual method invokes the method of its base class(es). Why would one do that? Encapsulation. A good example is the static invocation chain of base class destructors triggered by the virtual resolution of a derived class destructor. All the destructor calls except for the initial resolution are resolved statically. Without making the base class virtual destructors inline, we cannot take advantage of this. Does it make much of a difference? If the hierarchy is deep and there are many objects destructed, yes.

For another example that does not use destructors, imagine that we are designing a library lending material hierarchy. We've factored the material's location into the abstract LibraryMaterial class. While we declare its print function as a pure virtual function, we also provide a definition: it prints out the material's location.

class LibraryMaterial {
private:
MaterialLocation _loc; // shared data
// ... public:
// declares pure virtual function
inline virtual void print( ostream& = cout ) = 0;
}; // we actually want to encapsulate the handling of the
// location of the material within a base class
// LibraryMaterial print() method - we just don't want it
// invoked through the virtual interface. That is, it is
// only to be invoked within a derived class print() method inline void
LibraryMaterial::
print( ostream &os ) { os << _loc; }

Next we introduce a Book class; its print function outputs the title, author, and so on. Before this, it invokes the base class LibraryMaterial::print function to display the location information. For example,

inline void
Book::
print( ostream &os )
{
// ok, this is resolved statically,
// and therefore is inline expanded ...
LibraryMaterial::print(); os << "title:" << _title
<< "author" << _author << endl;
}

Our AudioBook class, derived from Book, introduces an alternative lending policy, and adds additional information such as narrator, format, and so on. These are displayed in its print function. Before this, it invokes Book::print(), which in turn, etc:

inline void
AudioBook::
print( ostream &os )
{
// ok, this is resolved statically,
// and therefore is inline expanded ...
Book::print();
os << "narrator:" << _narrator << endl;
}

In both this example and the example of the class destructor, the virtual method of the derived class incrementally expands on the functionality of its base class and involves a chain of invocations where only the initial invocation is resolved virtually. This unnamed hierarchical design pattern is significantly less effective if we never declare a virtual function to be inline.

What about the potential of code bloat cited in item (2)? Well, let's think about that. If we write,

LibraryMaterial *p =
new AudioBook( "Mason & Dixon",
"Thomas Pynchon", "Johnny Depp" );
// ...
p->print();

is this instance of print to be inlined? No, of course not. This has to be resolved at run-time through the virtual mechanism. Okay. Does it cause this instance of print to have its definition laid down? Also no. The call is transformed into something of the form:

// Pseudo C++ Code
// Possible transformation of p->print()
( *p->_vptr[ 2 ] )( p );

where 2 represents the location of print within the associated virtual function table. Because this call to print is done through the function pointer _vptr[2], the compiler cannot statically determine the location of the called function, and the function cannot be inlined.

Of course the definition of the inline virtual print function must appear somewhere in the executable for the code to run properly. That is, at least one definition is necessary in order for its address to be placed within the virtual table. How does the compiler decide when to generate that definition? One implementation strategy is to generate that definition at the same time the class virtual table is generated. That means that for each virtual table instance generated for a class, an instance of each inline virtual function is also generated.

Just how many virtual tables are actually generated within an executable for a class? Ah, well, that's a good question. The Standard makes requirements on the behavior of virtual functions; it does not make requirements on their implementation. Since the presence of a virtual table is not required by the Standard, obviously the Standard in turn makes no requirements on how the virtual table is handled or how many are generated. The optimal number, of course, is one. Stroustrup's original cfront implementation, for example, achieved that in most cases through cleverness. (Stan and Andy Koenig described the algorithm in the March 1990 C++ Report article, "Optimizing Virtual Tables in C++ Release 2.0.")

Moreover, the C++ Standard now requires that inline functions behave as though only one definition for an inline function exists in the program even though the function may be defined in different files. The new rule is that conforming implementations should behave as though only a single instance is generated. Once this aspect of the Standard is widely implemented, lingering concerns over potential code bloat from inline functions should disappear.

One of the tensions in the C++ community is the pedagogical need to present a simple checklist set of rules versus the practical need to apply rules judiciously based on the situational context. The former is a response to the complexity of the language; the latter, to the complexity of the solutions we need to construct. The problem of when to declare virtual functions inline is a good illustration of this tension.

Stanley Lippman was the software Technical Director for the Firebird segment of Disney's Fantasia 2000. He was recently technical lead on the ToonShooter image capture and playback system under Linux for DreamWorks Feature Animation and consulted with the Jet Propulsion Laboratory. He is currently IT Training Program Chair for You-niversity.com, an e-learning training company. He can be reached at stanleyl@you-niversity, www.you-niversity.com, and www.objectwrite.com.

Josée Lajoie is currently doing her Master's degree in Computer Graphics at the University Waterloo. Previously, she was a member of the C/C++ compiler development team at the IBM Canada Laboratory and was the chair of the core language working group for the ANSI/ISO C++ Standard Committee. She can be reached at jlajoie@cgl.uwaterloo.ca.

Standard C++ Programming: Virtual Functions and Inlining的更多相关文章

  1. [C++] OOP - Virtual Functions and Abstract Base Classes

    Ordinarily, if we do not use a function, we do not need to supply a definition of the function. Howe ...

  2. [CareerCup] 13.3 Virtual Functions 虚函数

    13.3 How do virtual functions work in C++? 这道题问我们虚函数在C++中的工作原理.虚函数的工作机制主要依赖于虚表格vtable,即Virtual Table ...

  3. [译]GotW #5:Overriding Virtual Functions

       虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你.如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题. Problem JG Ques ...

  4. Effective C++ Item 35 Consider alternatives to virtual functions

    考虑你正在为游戏人物设计一个继承体系, 人物有一个函数叫做 healthValue, 他会返回一个整数, 表示人物的健康程度. 由于不同的人物拥有不同的方式计算他们的健康指数, 将 healthVal ...

  5. Effective C++ Item 9 Never call virtual functions during constrution or destruction

    Because such calls would never go to a more derived class than that of currently executing construto ...

  6. 多重继承下的virtual functions

    有如下图所示的继承关系: 有如下代码示例:                   在早期的未符合c++标准的的编译器上是会报错的,因为对于clone()函数来说,编译器不知道怎么处理处理.但是时至今日c ...

  7. 条款35:考虑virtual函数以外的其他选择(Consider alternative to virtual functions)

    NOTE: 1.virtual 函数的替代方案包括NVI手法及Strategy设计模式的多种形式.NVI手法自身是一个特殊形式的Template Method设计模式. 2.将机能从成员函数移到外部函 ...

  8. 条款9:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)

    NOTE:在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

  9. R Programming week2 Functions and Scoping Rules

    A Diversion on Binding Values to Symbol When R tries to bind a value to a symbol,it searches through ...

随机推荐

  1. 简单分析Java的HashMap.entrySet()的实现

    关于Java的HashMap.entrySet(),文档是这样描述的:这个方法返回一个Set,这个Set是HashMap的视图,对Map的操作会在Set上反映出来,反过来也是.原文是 Returns ...

  2. SqlServer2008快照隔离模式的业务应用

    场景: 有200个检测点,每个检测点每天采集5个数据,对表的读写都是随机的(即有可能同时读写),总共有5年的数据. 存储方案A: 日期 点号 类型 值 20120101 001 A 1.0 20120 ...

  3. JMeter常用函数整理

    "_csvRead"函数 csvRead函数是从外部读取参数,csvRead函数可以从一个文件中读取多个参数. 下面具体讲一下如何使用csvread函数: 1.新建一个csv或者t ...

  4. ubnutu安装sougou 输入法

    先安百度经验安装fcitx 1.首先下载sogoupinyin_2.0.0.0068_amd64.deb,点击安装后,会通过ubuntu软件中心安装,安装玩成后,任然是无法使用.然后: 2.然后执行下 ...

  5. NodeJs编写小爬虫

    一,爬虫及Robots协议 爬虫,是一种自动获取网页内容的程序.是搜索引擎的重要组成部分,因此搜索引擎优化很大程度上就是针对爬虫而做出的优化. robots.txt是一个文本文件,robots是一个协 ...

  6. Javascript——Math对象

    Math 对象是一个固有的对象,无需创建它,直接把 Math 作为对象使用就可以调用其所有属性和方法.这是它与Date,String对象的区别  Math 对象属性 Math 对象方法  

  7. cmd命令行中的errorlevel和延迟赋值

    最近用到了命令行,一点心得: 1.errorlevel返回的确实是上一条命令的返回值,但不同命令的表现完全不同.比如: dir echo %errorlevel% //显示0 dir aldkalf ...

  8. SPOJ SUBLEX 求第k小子串

    题目大意: 对于一个给定字符串,找到其所有不同的子串中排第k小的子串 先构建后缀自动机,然后我们可以将整个后缀自动机看做是一个DAG图,那么我们先进行拓扑排序得到 *b[N] 对于每个节点记录一个sc ...

  9. Xutils的使用 转载 带自己细细研究

    单例模式static DbUtils db = null; public static DbUtils getDb(Context context) { if (context == null) { ...

  10. Android的R.java文件

    1.Android资源管理简介: Android应用程序资源可以分为两大类,分别放在assets和res文件夹下.assets目录下保存的是一些原始的文件,可以以任何方式来进行组织.这些文件最终会被原 ...