【Nim 小ネタ】C++のdynamic_cast相当のことを行う
この記事は「Nim Advent Calendar 2021」の16日目の記事として登録させてもらっています。
C++のdynamic_cast
C++にはdynamic_cast
というキャストがあり、これを使うと安全にダウンキャスト*1を行うことができます。
ここでいう「安全」とは、ダウンキャストの失敗を判断できるということです。
#include <stdio.h> class Base { public: virtual ~Base(){} }; class Derived : public Base {}; int main() { Derived* derived = new Derived; Base* base1 = derived; Base* base2 = new Base; printf( "dynamic_cast<Base*>(base1) = %p\n", dynamic_cast<Derived*>(base1) ); printf( "dynamic_cast<Base*>(base2) = %p\n", dynamic_cast<Derived*>(base2) ); return 0; }
出力結果
dynamic_cast<Base*>(base1) = 0x14a0010 dynamic_cast<Base*>(base2) = (nil)
変数base2
が指すインスタンスはDerivedクラスのインスタンスではないので、dynamic_cast<Derived*>
としてもnullptr
になります。
daynamic_cast
結果がnullptr
かどうかを判定することでダウンキャストに成功したかどうかを判断できます。
ちなみに、C++には()
を使ったキャストやstatic_cast
などもありますが、これらはダウンキャストに成功したかどうかが判断できません。
つまり安全なダウンキャストではありません。
Nimのキャスト
Nimでキャストを行うには、cast[型](値)
と記述します。しかし、このキャストでは安全にダウンキャストを行うことができません。
type Base = ref object of RootObj Derived = ref object of Base var derived: Derived = Derived() var base1: Base = derived var base2: Base = Base() echo "derived = " & derived.repr # derived = ref 0x7fd085acc050 --> [] echo "base2 = " & base2.repr # base2 = ref 0x7fd085acc070 --> [] echo "cast[Derived](base1) = " & cast[Derived](base1).repr # cast[Derived](base1) = ref 0x7fd085acc050 --> [] echo "cast[Derived](base2) = " & cast[Derived](base2).repr # cast[Derived](base2) = ref 0x7fd085acc070 --> []
変数base2
が指すインスタンスはDerivedクラスのインスタンスではないのにキャストに成功してしまっています。
このキャストでDerivedクラスのフィールドにアクセスすると不正な参照になってしまいます。
型変換
Nimにはキャストと似たような型変換という機能もあります。型変換を行うには型(値)
と記述します。
型変換を使って上記コードのキャスト部分を書き換えるとこうなります。
echo "Derived(base1).repr = ", Derived(base1).repr # Derived(base1).repr = ref 0x7fd085acc050 --> [] echo "Derived(base2).repr = ", Derived(base2).repr # Error: unhandled exception: invalid object conversion [ObjectConversionDefect]
型変換の場合は、キャストと違ってエラー(例外)が発生しました。この例外を使ってダウンキャストの成否を判断することができるので、型変換で安全にダウンキャストを行うことはできそうです。
しかし、ダウンキャストのために毎回例外を捕捉するのは面倒なので、別の方法を使います。
of演算子
of
演算子を使うことで、ある型がある型の派生型(あるいは同一型)であるかどうかを判定することができます。
上記コードの変数base1
、base1
それぞれにof
演算子でDerivedの派生型(あるいは同一型)かどうか問い合わせると正しくtrue
、false
が返ってきます。
echo base1 of Derived # true echo base2 of Derived # false
of演算子を使って安全にダウンキャストを行う
of
演算子の結果がtrue
の場合のみ、キャストを行うようにすれば安全にダウンキャストが可能になります。
例えば以下のようにします。
var d: Derived = if base1 of Derived: cast[Derived](base1) else: nil
dynamic_castを実装する
安全にダウンキャストを行う方法が見つかったので、これをdynamic_cast[Derived](base1)
のような形で簡単に記述できるようにしたいと思います。
template dynamic_cast*[T]( x: untyped ): T = if x of typedesc[T]: cast[T](x) else: nil
使い方
var d: Derived = dynamic_cast[Derived](base1)
このdynamic_castテンプレートを使えば、C++のdynamic_castと同じように、ダウンキャストに失敗した場合はnil
になります。
これでNimでもC++のようなdynamic_cast
ができるようになりました。
おまけ
C++ではdynamic_cast
の結果をif文で変数に格納することで、ダウンキャストに成功したときの処理をスマートに記述することができました。
if ( Derived* derived = dynamic_cast<Derived*>(base1) ) { // derivedを使って何かする }
似たようなことをNimでやるには以下のようにします。
if (var derived = dynamic_cast[Derived](x); derived != nil): # derivedを使って何かする
Nimは(式1;式2)のように記述することができ、全体の評価結果としては式2になるので上記のようなことが可能になっています。
これをtemplate化すればもうちょっと楽に記述できるようになります。
template if_derived*[T]( x: untyped, body: untyped ) = if (var derived {.inject.} = dynamic_cast[T](x); derived != nil): body
使い方
if_derived[Derived](base1):
# derivedを使って何かする
これを使った場合、elseが使えないので注意してください。
以上おまけでした。
*1:基底クラス型を派生クラス型へ変換すること