Flat Leon Works

アプリやゲームを作ってます。

【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演算子を使うことで、ある型がある型の派生型(あるいは同一型)であるかどうかを判定することができます。

上記コードの変数base1base1それぞれにof演算子でDerivedの派生型(あるいは同一型)かどうか問い合わせると正しくtruefalseが返ってきます。

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:基底クラス型を派生クラス型へ変換すること