SystemVerilogには非仮想関数なんて必要ないというのが本記事の趣旨です。
SystemVerilogのメソッドには仮想関数と非仮想関数がありますが、非仮想関数を使う理由は無いと筆者は考えます。全てのメソッドにvirtualを付けて仮想関数にすることを推奨します(virtualを付けられないnew()とstaticメソッド、それにスコープがクラス内に限られるlocalメソッドは除きます)。
仮想関数と非仮想関数の違いの再確認
両者の動作の違いを再確認します。基底クラスにvirtualがついているメソッドを仮想メソッドあるいは仮想関数と呼びます。一度virtualで宣言されたメソッドは、派生クラスではどこまでも仮想関数となります。
仮想関数の場合、基底クラスのメソッドが派生クラスで再定義された場合、常に派生クラスのメソッドが呼び出されます。これはメソッドのオーバーライドと呼ばれます。下の例の場合、インスタンス化したクラスの型がDerivedなのでbase.declare()とderived.declare()両方で同じ結果が出力されます。
class Base;
virtual function void declare();
$display("Base");
endfunction
endclass
class Derived extends Base;
function void declare();
$display("Derived");
endfunction
endclass
program top();
initial begin
Base base;
Derived derived;
derived = new();
base = derived;
base.declare(); // Derived
derived.declare(); // Derived
end
endprogram
非仮想関数の場合、基底クラスのが派生クラスで再定義された場合、基底クラスと派生クラスどちらのメソッドが呼び出されるかは、そのオブジェクトを参照している変数の型で決まります。下の例はBaseクラスのdeclare()メソッドに付いていたvirtualを外したものです。上記プログラムとの違いはこの一行のみです。
参照しているクラスの実体は同じものなのに、変数の型で呼び出されるメソッドが変わります。
class Base;
// virtual function void declare();
function void declare();
$display("Base");
endfunction
endclass
class Derived extends Base;
function void declare();
$display("Derived");
endfunction
endclass
program top();
initial begin
Base base;
Derived derived;
derived = new();
base = derived;
base.declare(); // Base
derived.declare(); // Derived
end
endprogram
非仮想関数の用途
SystemVerilogのClass関連の機能はC++の影響を強く受けており、非仮想関数もC++由来のものと思われます。さて、前述のように非仮想関数を派生クラスで再定義した場合、参照している変数の型で呼び出されるメソッドが変わる機能ですが、どのような用途があるでしょうか?
実は筆者の知っている限り用途はありません。非仮想関数は、派生クラスでは再定義しないのが通例です (Effective C++ 3版の第36項でも同様のことが書かれています。 ``36項 継承した非仮想関数を再定義してはならない'')。では何故C++で非仮想関数があるかというと、それは高速化のためです。原理的に非仮想関数は仮想関数より高速に呼び出すことが可能なのです(この点については次で簡単に説明します)。
これに対して多くのスクリプト言語、例えばRuby, Python, PHPなどは非仮想関数自体が言語仕様にありません。高速化よりも記述の簡潔さを優先しているためだと思われます。SystemVerilogも言語の用途としてはC++のようなぎりぎりまで高速化を追求するものではありませんので、非仮想関数のメリットは少ないと言えます。全てを仮想関数とすることで、宣言し忘れのトラブルを防止することができます。
なぜ非仮想関数は仮想関数より早く呼び出せるのか
命令コードのメモリ配置が関わってきます。以下の説明はあくまで原理的なもので、実装は違っている可能性があります。
メソッドの命令コードは全てのインスタンスで共用できます。Base.declare()の実行コードががメモリ上のアドレス0x100にDerived.declare()の実行コードがメモリ上のアドレス0x200に配置されているとしましょう。
これらを呼び出す以下のようなコードを考えます。
initial begin
Base base;
base = new();
if ($urandom_range(1)) begin
Derived derived;
derived = new();
base = derived;
end
base.declare(); // What is the result?
end
非仮想関数の呼び出し
declare()が非仮想関数だった場合を考えます。変数baseの型のみでどのクラスのメソッドを呼び出せば良いのかが決められるため、メソッド実行時のメモリアドレスがコンパイル時に確定できます。baseの型がBaseであればアドレス0x100を呼び出し、Derivedであれば0x200を呼び出すだけです。本例の場合は0x100に決め打ちできます。
仮想関数の呼び出し
declare()が仮想関数だった場合を考えます。変数baseが参照しているオブジェクトのクラスにより、どのメソッドを呼び出すかを判断しなければなりません。プログラムの進行により変数の参照先のオブジェクトが変更されるため、コンパイル時に決定することができません。base.declare()実行時に以下の手順を踏む必要があります。
- 変数の指しているオブジェクトのクラスを判別する
- 判別したクラスに対応したメソッドを呼び出す(Baseなら0x100, Derivedなら0x200)
以上から、仮想関数の呼び出しは非仮想関数の呼び出しよりも手順が一つ増えてしまい、そのぶん呼び出しが遅くなるのです。ただし、C++ならともかくSystemVerilogではこの差はプログラムのその他の部分の処理時間に比べて相対的に小さいため、無視できる時間になるでしょう。
C++の一般的な実装ではにこのための情報はテーブルに格納されており、これを仮想関数テーブルあるいはvtableと呼びます。Wikipediaだとここ、Qiitaだとここに詳しく説明されていますので興味があるかたは読んでみてください。
参考文献
"8.20 Virtual methods". 1800-2017 - IEEE Standard for SystemVerilog--Unified Hardware Design, Specification, and Verification Language. pp. 229-231.