「新しい Win64 (モダン) C++ ツールチェーンへアップグレードしたい」というお問い合わせをエンバカデロのサポートへお寄せいただいております。その中には、リリース モードでビルドしたアプリを実行するとクラッシュする事例が報告されています。例えば、以下のようなエラーが表示されましたが、このエラーの理由がわからないという内容です。
[crayon-673fafa64c85c025647075/]確かに上記のエラーから理由を判断するのは難しいです。本ブログでは、これまでエンバカデロが遭遇してきたすべてのケースから何が起こっているのか、その解決方法やヒントとなる情報をご説明いたします。
未定義の挙動
これまでに遭遇してきたケースはいずれも、初期化されていない変数へアクセスしていることが同様の理由でした。以下に 2 つの例を示します。
[crayon-673fafa64c866885033084/]Clang の古いバージョンと従来のBorlandコンパイラでは、この点においては寛容で、エラーにはならず、コードは正常に実行できました。もし変数がスタック上にある場合は、その値は実質的にランダムな値になります 。(変数が常に0以外、つまり false 以外の値を持っている可能性があるため、if文の boolチェックが正しく実行されているように見える理由です。)
ただし、初期化されていないデータを含むコードは未定義の振る舞いになり、これは予期しない動作に繋がります。新しいClangでは、最適化を重要課題として取り組んでいるため、実行するコードが正しいことを認識する必要があります。過去に初期化されていない変数をチェックするif文を無視して、常にif文の本体を実行するコンパイラがあったという記事を読んだことがある方もいらっしゃるかもしれません。
では、別の例を見てみましょう。
[crayon-673fafa64c869794173323/]上記のコードもクラッシュしますが、初期化されていないポインタを逆参照しているわけではないことに注意してください。 ランダムな値を再参照すれば、たいていの場合クラッシュするはずです。上記のコードではそのようなことは行っていませんが、 bcc64xをReleaseモード(最適化オン)でコンパイルしたコードは、実行時にクラッシュします。(ただし、Debugモード(最適化オフ)でコンパイルしたコードは、実行時にクラッシュしません。)
なぜかというと、Clangは他のツールチェーンよりも少し厳格で、未定義の振る舞いを動作させないようにしているからです。つまりクラッシュします。上記のコードのように意図的なトラップコードを挿入すると、クラッシュさせることができます。Clangの内部的な判断についてはわかりませんが、これはコードが到達可能であれば有効であることをオプティマイザに強制する直接的な方法である可能性が考えられます。
一方、100万行のコードがあって、その中に初期化されていない変数が複数箇所に点在している場合、コードがクラッシュして原因がわからないのでは困ります。なぜなら、次節で紹介するステップを踏まない限り、それは明らかではないからです。
解決:予想よりは簡単
幸いなことに、この挙動を事前に見つけるのは非常に簡単なので、解決することは実に簡単です。
Clangは初期化されていない変数に対してコンパイラ警告を出すことができますが、Clang はデフォルトでは警告は出しません。これはClang側の挙動にはなりますが、単にクラッシュさせるよりも警告を出したほうが有益であるため、 本来はClang でもデフォルトで警告出すべきだと考えます。
一般的に、エンバカデロでは正当な理由がなければデフォルトの動作を変更しません。また、Clang ではデフォルトが適切であると信頼する部分もあったため、あえてこの設定は変更しなかった理由です。
しかし、振り返ってみると、予期しないアプリのクラッシュを回避するのに役立つことは正当な理由であると判断いたしました。 そこで12.2 では、デフォルトで警告を出す機能を有効にする方向で検討しています。
免責事項: このブログ記事で説明しているRAD Studioの将来のバージョンにおける新機能や改善点は、いずれも開発が完了しGA版がリリースされるまでコミットされません。
デフォルトを変更するオプションは多数用意されていますが、警告オプションを有効にするには、以下のようなシンプルに設定できる方法をお勧めいたします。
[crayon-673fafa64c86b433560999/]つまり、すべての診断を付けてコンパイルし、それをチェックするだけです。 コンパイラが警告を出した場合、99% 正しい結果が得られます。 残りの1%は、#pragmaを使用して警告を無効にするだけです。
[crayon-673fafa64c873313473490/]初期化されていない変数についての警告が表示された場合、それはそのコードが最適化されると実行時にクラッシュするという警告でもあります。
そのため正しいコードを実行するためには、常に変数を初期化することです。
[crayon-673fafa64c875589875832/]上記のサンプルコードの例では、以前は初期化されていなかった変数が、それぞれfalseとnullptrに初期化されています。
変数の初期化を行う対象ですが、上記のコード スニペットのようなローカル変数だけでなく、グローバル変数、フィールドなど、すべての変数に対して実行する必要があります。
利点
コードが警告なしでコンパイルできるようになれば、アプリの実行時の動作はさらに信頼性を増します。もしお客様のプロジェクトコードが一度も警告を受けたことがなければ、多数のエラーが発生する可能性があります。 一度コンパイルして、全部を直さなくても、コードの安全性を担保するためにも、最低でも一度は警告が表示されないかチェックをいただくことを強くお勧めします。
(奇妙なことに、警告が出れば出るほどコンパイルが遅くなる可能性があります。そのため、状況によって異なりますが、警告を取り除くことでビルド速度が向上することもあります。)