C++中的访问者模式在运行时识别一个对象的具体类型是需要一定的开销的,这个开销就是虚函数。

我们写一些代码来看看效果:

#include
using namespace std;

class Visitor;

class Animal {
public:
    void accept(Visitor * v) {cout << "emmm.." << endl;};
};

class Dog : public Animal {
public:
    void accept(Visitor * v); 
};

class Cat : public Animal {
public:
    void accept(Visitor * v); 
};

class Fox : public Animal {
public:
    void accept(Visitor * v); 
};

class Visitor {
public:
    void visit(Animal * animal) { cout "hooa~" << endl;}
    void visit(Dog * animal) {}
    void visit(Cat * animal) {}
    void visit(Fox * animal) {}
};

class Speaker : public Visitor {
public:
    void visit(Animal * pa) {
        pa->accept(this);
    }

    void visit(Dog * pd) {
        cout << "wang~" << endl;
    }

    void visit(Cat * pc) {
        cout << "miao~" << endl;
    }

    void visit(Fox * pf) {
        cout << "woo~" << endl;
    }
};

void Dog::accept(Visitor * v) {
    v->visit(this);
}

void Cat::accept(Visitor * v) {
    v->visit(this);
}

void Fox::accept(Visitor * v) {
    v->visit(this);
}

int main() {
    Animal * animals[] = {new Dog(), new Cat(), new Fox()};
    Speaker s;
    for (int i = 0; i < sizeof(animals) / sizeof(Animal *); i++) {
        s.visit(animals[i]);
    }
    return 0;
}                     

我们来编译运行这个程序看一下效果:

# g++ -o s speaker.cpp 
# ./s 
emmm..
emmm..
emmm..

怎么样?结果和你预料的一样吗?

为什么会这样呢?这是因为C++语言不同于Java,C++全部是静态类型。也就是说C++在编译阶段就已经决定了调用哪个函数,而不是在运行阶段决定的。具体来说:

s.visit(animals[i]);

这一行语句,调用的方法一定是:

void visit(Animal * pa) {
        pa->accept(this);
    }

这一点与Java还是一样的,还比较容易理解。但接下来,accept方法就有所有不同了。在Java中,调用pa的accept方法,会根据pa的具体类型进行动态分派。但在C++中,却不会这样。当编译器分析到这里,它看到的pa就是Animal *类型的。那么pa->accept就会静态绑定到Animal::accept方法,而不会根据pa的真实类型去做动态分派(我们也叫它运行时识别)。

那在C++中怎么解决这个问题呢?答案就是虚函数,在函数的定义前加上virtual关键字。那么这个函数在继承体系中,就具备了动态分派,或者叫做运行时识别的能力。简单来说,就是这样:

class Visitor;

class Animal {
public:
    virtual void accept(Visitor * v) {cout << "emmm.." << endl;};
};

class Dog : public Animal {
public:
    virtual void accept(Visitor * v);
};

class Cat : public Animal {
public:
    virtual void accept(Visitor * v);
};

class Fox : public Animal {
public:
    virtual void accept(Visitor * v);
};

class Visitor {
public:
    void visit(Animal * animal) {}
    virtual void visit(Dog * animal) {}
    virtual void visit(Cat * animal) {}
    virtual void visit(Fox * animal) {}
};

class Speaker : public Visitor {
public:
    void visit(Animal * pa) {
        pa->accept(this);
    }

    virtual void visit(Dog * pd) {
        cout << "wang~" << endl;
    }

    virtual void visit(Cat * pc) {
        cout << "miao~" << endl;
    }

    virtual void visit(Fox * pf) {
        cout << "woo~" << endl;
    }
};

可以看到,整个程序其他的地方全都没改,只是把accept方法和visit方法变成了virtual而已。那么我们再运行这个程序看一下:

# g++ -o v visitor.cpp 
# ./v 
wang~
miao~
woo~

这一次就能正确地打印了。

双重分派的缺点

有同学可能会有疑惑,为什么C++不默认把动态分派做为一种语言特性,而是要通过virtual关键字来手动打开呢?(大家理解一下,其他Java就是把动态分派做为默认的语言特性,相当于说Java中所有的方法都是virtual的)

这主要是出于性能上的考虑。因为C++是要直接编译生成二机制机器码的(初学者的话,就理解成汇编代码吧)。在编译阶段就能确定知道我要调用的函数是哪一个,那就把这个函数的地址直接填到 call 指令那里,这样无疑是最快的。而如果是动态分派的话,就需要在运行时维护一些额外的数据结构(后面我会介绍Java对象和C++对象在内存中的布局,这样大家就能明白这个需要额外维护的数据结构是什么了)。而维护这个数据结构,会使性能受到很大的影响。所以在C++中开启virtual是一件需要进行权衡和取舍的事情。

而反观Java,由于Java的优化编译是运行时执行的。能把这些额外的数据结构最后优化到JIT代码中去,反倒不用担心这个问题了。从这里也可以看出,那些觉得Java运行速度慢的人,很多人并不了解事情的真相,只是人云亦云罢了。Java的快或者慢都必须拿到特定的场景下去讨论,Java有时比C++运行更快,也不是不可能。

JVM中的实际例子

我的课程中的设计模式都是分开来讲的,用到哪一部分就讲哪一部分。当时,会讲访问者模式是因为在GC算法中,使用了OopClosure这一个典型的访问者模式。我们还是再回到出发点上来。

我们看这节课中的例子,Copy GC(3) : JVM中的实现:

void DefNewGeneration::
oop_since_save_marks_iterate_v(OopsInGenClosure* cl) {
  cl->set_generation(this);
  // 前面的注释讲了_saved_mark_word在eden和from中是没有用的。
  // 下面第一,第三行删掉也不会出错。这两行算是遗留代码了。
  eden()->oop_since_save_marks_iterate_v(cl);
  // 只有这一行有用。
  to()->oop_since_save_marks_iterate_v(cl);
  from()->oop_since_save_marks_iterate_v(cl);
  cl->reset_generation();
  save_marks();
}

oop_since_save_marks_iterate_v的作用就是遍历to space中的每一个对像。这和我们例子中,遍历数组,对数组中的每一个对像执行visit方法是一样的。现在呢,Visitor的实例就是代码中的cl。cl 的类型是 OopsInGenClosure,它继承自 OopClousre,而OopClosure的定义是这样的:

class OopClosure : public Closure {
 public:
  virtual void do_oop(oop* o) = 0;
  virtual void do_oop_v(oop* o) { do_oop(o); } 
  virtual void do_oop(narrowOop* o) = 0;
  virtual void do_oop_v(narrowOop* o) { do_oop(o); }
};

这个定义就是说,如果你想对现在正在访问的对象 o 做什么操作的话,那就把这个操作封装到一个Closure中。