虚函数
引言
C++ 里有虚函数这个概念,虚函数根据参数的类型,从一组函数中选择一个调用。注意,这里将对象方法理解为第一个参数是对象本身的函数。
为了支持虚函数特性,C++ 会在使用多态特性的时候设置虚函数表。虚表在背后起作用,为了理解它的作用,可以在 scheme 中模拟这个过程。
Scheme 没有静态类型系统,所以需要给变量打上类型标签。
;; tag operations
(define (attach-tag type-tag contents)
(cons type-tag contents))
(define (type-tag datum)
(if (pair? datum)
(car datum)
(error "Bad tagged datum -- TYPE-TAG" datum)))
(define (contents datum)
(if (pair? datum)
(cdr datum)
(error "Bad tagged datum -- CONTENTS" datum)))
虚表
可以将虚表理解为一个映射:(操作名 参数类型列表)-> (具体函数),从这里就可以看出,虚函数其实是一组操作的名称,虚函数在概念上是一种通用型操作,对不同类型的对象可能有不同的行为。
;; v table operations
(define vtable (make-hash))
(define (put op type item)
(hash-set! vtable (list op type) item))
(define (get op type)
(hash-ref vtable (list op type)))
为了让虚函数利用虚表,需要先提取参数的类型,用虚函数和参数类型从虚表中取出相应函数。
;; vtable lookup
(define (apply-generic op . args)
(let* ((type-tags (map type-tag args))
(proc (get op type-tags)))
(apply proc (map contents args))))
最后,将得到的函数应用在脱去标签的数据上。注意,虚表中取出的不一定就是最终作用在数据上的函数,也有可能是另一个虚函数,将它应用在数据上会引发另一次虚表查找。这种基于标签的分发可以无限地进行下去。
下面是一些虚函数地例子:
;; virtual functions
(define (add x y)
(if (and (number? x) (number? y))
(+ x y)
(apply-generic 'add x y)))
(define (sub x y)
(if (and (number? x) (number? y))
(- x y)
(apply-generic 'sub x y)))
(define (mul x y)
(if (and (number? x) (number? y))
(* x y)
(apply-generic 'mul x y)))
(define (div x y)
(if (and (number? x) (number? y))
(/ x y)
(apply-generic 'div x y)))
(define (equ? x y)
(if (and (number? x) (number? y))
(= x y)
(apply-generic 'equ? x y)))
(define (=zero? x)
(if (number? x)
(zero? x)
(apply-generic '=zero? x)))
具体函数可以通过如下方式注册到虚表上:
;; rational-package
(define (install-rational-package)
;; internal procedures
(define (numer x) (car x))
(define (denom x) (cdr x))
(define (make-rat n d)
(let ((g (gcd n d)))
(cons (/ n g) (/ d g))))
(define (add-rat x y)
(make-rat (+ (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
(define (sub-rat x y)
(make-rat (- (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
(define (mul-rat x y)
(make-rat (* (numer x) (numer y))
(* (denom x) (denom y))))
(define (div-rat x y)
(make-rat (* (numer x) (denom y))
(* (denom x) (numer y))))
(define (equ? x y)
(and (= (numer x) (numer y))
(= (denom x) (denom y))))
(define (=zero? x)
(= (numer x) 0))
;; interface to rest of the system
;; add to v table
(define (tag x) (attach-tag 'rational x))
(put 'add '(rational rational)
(lambda (x y) (tag (add-rat x y))))
(put 'sub '(rational rational)
(lambda (x y) (tag (sub-rat x y))))
(put 'mul '(rational rational)
(lambda (x y) (tag (mul-rat x y))))
(put 'div '(rational rational)
(lambda (x y) (tag (div-rat x y))))
(put 'equ? '(rational rational)
(lambda (x y) (equ? x y)))
(put '=zero? '(raional)
(lambda (x) (=zero? x)))
(put 'make 'rational
(lambda (args)
(let ((n (car args)) (d (cadr args)))
(tag (make-rat n d))))))
虚表的分解
问题的核心是清晰的:要通过某种方式,根据操作名称和操作数类型决定操作。借助一个全局的查找表,让不同的类型在其中填写和自己有关的部分,就实现了。
请想象,将虚表按类型拆分,放在不同的类型里。对数据应用函数变为调用数据对象的分派函数并传入函数名。这就是面向对象语言的一般做法。