C/C++インタプリタ CINTのデストラクタ不正呼び出し問題と格闘した話 その3

## 以前の記事

[その1](https://qiita.com/NCScript/items/b4f0d244c8392762b802)
[その2](https://qiita.com/NCScript/items/ff68760381995fe76947)

## 再び実験

ROOT 5で以下のプログラムを実行してみます。内容は有名なFizzBuzz問題です。

```C++:FizzBuzz.cpp
#include "my_string.h"
#include <cstdio>

using namespace std;

void FizzBuzz()
{
    for (int i = 1; i <= 100; ++i) {
        my_string message;

        if (i % 3 == 0) {
            const my_string s1 = "Fizz";
            message += s1;
        }

        if (i % 5 == 0) {
            const my_string s2 = "Buzz";
            message += s2;
        }

        if (message.empty()) {
            const my_string s3(i);
            message += s3;
        }

        printf("%s\n", message.c_str());
    }
}
```

ソースファイルをロードして`FizzBuzz`関数を実行してみます。`my_string.cpp`と`my_string.h`は、[その1](https://qiita.com/NCScript/items/b4f0d244c8392762b802)で使用した手抜き文字列クラスを別のソースファイルに分割して、文字列が空であればtrueを返すメンバー関数`empty`を追加した物です。
![Image1.png](https://qiita-image-store.s3.amazonaws.com/0/297026/c8af7d65-c370-a930-44a1-fc80e3583eb0.png)
特にエラーも無く、一見正常に実行されたように見えます。
![Image2.png](https://qiita-image-store.s3.amazonaws.com/0/297026/a325fa42-95c2-a267-c189-d146bfc4cf6c.png)
もう一度`FizzBuzz`関数を実行してみます。すると、ROOTのプロセスが異常終了してしまいました。原因は何なのでしょうか?
![Image3.png](https://qiita-image-store.s3.amazonaws.com/0/297026/8f498540-f886-4ec6-3e98-67c3831bcd01.png)

## 機能制限

CINTには言語機能にいくつかの制限があり、C++標準規格との間に非互換性が存在しています。例えば、[ブロックスコープ内で宣言した変数の寿命が関数の終わりまで存続する](https://www-he.scphys.kyoto-u.ac.jp/member/n.kamo/wiki/doku.php?id=study:software:root:cint#%E5%A4%89%E6%95%B0%E3%81%AE%E5%AF%BF%E5%91%BD%E3%81%8C%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%AE%E7%AF%84%E5%9B%B2%E5%A4%96%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E3%82%82%E3%81%A4%E3%81%8D%E3%81%AA%E3%81%84)、[同じ型であれば同じ名前の変数を再宣言できる](https://www-he.scphys.kyoto-u.ac.jp/member/n.kamo/wiki/doku.php?id=study:software:root:cint#%E5%90%8C%E3%81%98%E5%A4%89%E6%95%B0%E5%9E%8B%E3%81%AA%E3%82%89%E4%BD%95%E5%9B%9E%E5%A4%89%E6%95%B0%E5%AE%A3%E8%A8%80%E3%81%97%E3%81%A6%E3%82%82error%E3%82%92%E5%87%BA%E3%81%95%E3%81%AA%E3%81%84)、等の非互換性がROOTの利用者の間ではよく知られています。

```C++
if (cond) {
    A obj(0);
    obj.func();
}

// condが真だった場合は, ここでも変数objを使用できる.
// obj.func();

// condが真だった場合は, 変数objの再宣言が行われる.
// 再宣言時には, コンストラクタの前にデストラクタが呼び出される.
A obj(1);

// これは型が異なるため, エラーになる.
// B obj(2);

// 1回目のループを除き, 変数obj2の再宣言が行われる.
// 再宣言時には, コンストラクタの前にデストラクタが呼び出される.
for (int i = 0; i < 10; ++i) {
    A obj2(i);
    obj2.func();
}
```

## バイトコード

CINTにはループをバイトコードにコンパイルして高速化する機能があります。この機能を使用する場合、通常のインタプリタで1回目のループを実行しながらバイトコード命令を出力して、2回目以降のループはバイトコードインタプリタで実行されるという流れになります。

この時、1回目のループで実行されなかったブロックもバイトコードにコンパイルされ、そのブロック内で定義されている変数は、コンストラクタが呼び出されていない状態で変数管理テーブルに登録されます。

```C++
for (int i = 0; i < 10; ++i) {
    if (i < 5) {
        // 1回目のループでは, 変数管理テーブルへの登録とコンストラクタの呼び出しが行われる.
        A obj1(1);
        obj1.func();
    } else {
        // 1回目のループでは, 変数管理テーブルへの登録のみ行われる.
        A obj2(2);
        obj2.func();
    }
}
```

ROOTの`.O 0`コマンドを使用(または、ソースファイルに`#pragma optimize 0`を追加)してバイトコード機能を無効にしていた場合は、1回目のループで実行されなかったブロックは単に読み飛ばされるため、変数管理テーブルへの登録は行われません。

## 原因

今回の異常終了の原因は、バイトコードインタプリタでの実行中に変数の再宣言が行われた場合、未初期化の変数に対してデストラクタが呼び出されてしまうことです。この問題を回避するには、以下の処理の追加が必要です。

* バイトコードへのコンパイル時に、実行されないブロック内の変数の`type`を`'u'`以外の値に差し替える。
* バイトコードインタプリタからのコンストラクタの呼び出し前に、未初期化の変数の`type`を`'u'`に戻す。

また、バイトコードインタプリタでコンストラクタの引数の評価時にエラーが発生した場合にも、`type`の差し替え処理が必要になります。

次回の記事(その4)では、この問題の解決のために行ったCINTのソースコードへの修正について解説したいと思います。

[その4](https://qiita.com/NCScript/items/714fa3fd88ec506af310)