警告
見方によっては、警告というのは言語デザインの破綻の表れです。 しかし別の見方では、'lint' のような、 潜在的に問題となりうる箇所を教えてくれる便利なツールでもあります。 ほとんどの場合、警告が指摘するポイントというのは、 意図した通りの問題ないコードです。しかしまれに、 プログラマの意図していないエラーを指し示してくれることもあります。
警告は、プログラミング言語Dの定義の一部ではありません。 コンパイラベンダに任されている部分であり、 ベンダによって何が警告となるかは異なります。 コンパイラによって警告が出される可能性のあるコードは、 Dのコードとしては合法なものです。
以下には、Digital Mars D コンパイラで -w スイッチを有効にしたときに出力される警告を示します。 ほとんどの項目は、エラーとすべきか警告とすべきか、 あるいはそのような場合の正しいDのコードの書き方はどうあるべきか、 長い論争がなされてきたものです。誰もが同意する解決策は得られていない問題ですから、 警告というオプションとして用意しました。正しいコードの書き方についても、 何通りかの選択肢を提示します。
warning - implicit conversion of expression expr of type type to type can cause loss of data
D では、式の中の整数昇格は C や C++ と同じ規則を採用しています。 これは互換性のためでもありますし、 近年のCPUはこのような規則を効率的に実行できるように設計されているためでもあります。 これらの規則には、暗黙の狭化変換も含まれています。 狭化変換とは整数演算の結果を小さい型へ戻すことを言いますが、 この時に有効ビットの情報が失われる可能性があります:
byte a,b,c;
...
a = b + c;
b と c は双方とも、標準の整数昇格規則に従って、 int へと拡張されます。加算の結果も、 int です。a へ代入されるときに、その int が暗黙に byte へ変換されます。
これがバグになりうるかどうかは、プログラムに依存します。 b と c がオーバーフローの可能性のある値であるか、 またオーバーフローがアルゴリズムに影響を与えるかどうか、など。 この警告を解消するには…:
- キャストを挿入する:
a = cast(byte)(b + c);
これによって警告は出なくなりますが、 型システムの抜け道であるキャスト命令を使うため、将来的にバグを隠してしまう可能性があります。 具体的には、後で a や b や c の型が変わったときや、 そもそも型がテンプレートのインスタンス化によってセットされている場合 (この場合、cast(byte) は cast(typeof(a)) となっているでしょう) です。 - a の型を int に変更する。 これは一般的には良い解決策ですが、もちろん aが小さな型でなければならない時には使えません。その場合は…
- オーバーフローの可能性を実行時に検査する:
byte a,b,c; int tmp; ... tmp = b + c; assert(cast(byte)tmp == tmp); a = cast(byte)tmp;
これによって、有効ビットが失われないことが保証されます。
言語側の変更による対処策として考えられているのは:
- 選択肢3の実行時チェックと同等のコードを、 コンパイラが自動的に挿入するようにする。
- 一般のキャストを制限した新しい構文 implicit_cast(type)expression を導入して、普通に暗黙のキャストが許される場面でのみ動作する、 と定義する。
- implicit_cast をテンプレートで実装する。
warning - array 'length' hides other 'length' name in outer scope
配列の添え字やスライスの [ ] の内側では、 その配列の長さを表す変数 length がセットされます。 length のスコープは、 '[' から ']' までです。
外のスコープで length という名前の変数が宣言されていた場合、 そちらの変数を [ ] の中で参照したいことがあるかもしれません。 例えば:
char[10] a; int length = 4; ... return a[length - 1]; // a[3] ではなく a[9] を返す
この警告を解消するには…:
- 外側の length を別の名前に変える。
- [] の内側の length を a.length に変える。
- もしlength がグローバルやクラススコープの変数ならば、 曖昧さの回避のために .length や this.length を使う。
言語側の変更による対処策として提案されているのは:
- length を暗黙に宣言される変数ではなく、 特別なシンボルないしは予約語としてしまう。
warning - no return at end of function
以下の例を考えます:
int foo(int k, Collection c) { foreach (int x; c) { if (x == k) return x + 1; } }
仮定として、アルゴリズムの性質として c の中に必ず x が見つかるとわかっているとしましょう。つまり、この foreach 文が最後まで回りきってしまうことはありません。 この関数の最後には return 文がありません。なぜなら、 その点に実行パスが到達することはないとわかっているため、 書いてもデッドコードとなってしまうからです。 これは完全に合法なコードです。 このようなコードのロバスト性を高めるために、Dコンパイラは、 関数の最後に assert(0; を挿入することで上のコードを正当化します。 すると、このforeachが最後まで回ることがないという前提に誤りがあれば、 assert失敗として実行時に明確に検出することができます。 これは単にそのまま実行を続けて色々なエラーを引き起こしたりするよりも、 望ましい動作と言えます。
Dのこの動作は、しかし、他の言語ではあまり見られない動作です。 このため、Dのこの動作に頼るのは落ち着かないという方や、 実行時よりはコンパイル時にエラーを出して欲しいというプログラマもいます。 foreach が途中で終わることが確実ならば、 そのことがソースコード上に明確に記述されているべきである、というわけです。 そこで、警告です。
この警告を解消するには…:
- 何か適当な値を返す(結局実行されない)
return 文を、
関数の最後に挿入する:
int foo(int k, Collection c) { foreach (int x; c) { if (x == k) return x + 1; } return 0; // return文がないという警告を抑制 }
この方法は驚くほどに広く使われています - 特に、 プログラマが未熟な時や、急いでいるときに。 これは非常に悪い解決策です。 問題は、バグがあって foreach が最後まで回りきってしまった時に露わになります。 この場合バグが検出されずに、 関数は予期せぬ値を返すことになり、 また別の問題を引き起こしたり、バグの存在がわからなくなってしまったりします。 もう一つの問題点は、他のプログラマが保守のためにこのコードを見たときです。 上のコードを分析してみると、決して実行されるはずのない return 文が置かれている謎のコードに見えます。 (コメントを入れればよいという考え方もありますが、コメントというのは見過ごされたり、 古くなってしまったり、間違ってたりするものです。) デッドコードはいつもプログラムの保守の際に混乱の元になりますから、 やみくもに挿入するのは悪いアイデアです。 - よりよい解決策は、D言語が暗黙のうちに行うことを明示的にやることです -
つまり、assert を挿入します:
int foo(int k, Collection c) { foreach (int x; c) { if (x == k) return x + 1; } assert(0); }
こうすると、foreach が最後まで回ってしまった場合、エラーが検出されます。 さらに、コード自体が説明になっている良いコードです。 - 他の選択肢としては、独自のエラークラスにユーザーに優しいメッセージを添えて
throw するという手があります:
int foo(int k, Collection c) { foreach (int x; c) { if (x == k) return x + 1; } throw new MyErrorClass("foo() の最後まで到達してしまいました"); }
warning - switch statement has no default
return 文がない場合の警告と同じように、switch 文に default がない場合の警告もあります。例:
switch (i) { case 1: dothis(); break; case 2: dothat(); break; }
D言語の意味論を考えると、このコードは、 i の取り得る値が 1 か 2 しかないことを主張しています。それ以外の全ての値は、 プログラムのバグです。このバグを検出するために、 コンパイラは次のように書いたのと同様にswitch文をコンパイルします:
switch (i) { case 1: dothis(); break; case 2: dothat(); break; default: throw new SwitchError(); }
これは、C/C++の動作とは大きく異なります。 C++では次のように書かれたswitch文と同じ動作をすることになっています:
switch (i) { case 1: dothis(); break; case 2: dothat(); break; default: break; }
うっかりcaseを一個抜かしてしまったり、プログラムのある箇所で値を追加したのに 他の場所を見逃してcaseを増やすのを忘れる、というのはよくある誤りですから、 この件周りは潜在的なバグの温床となりやすいのです。 Dはこの手のエラーを実行時に std.switcherr.SwitchError を投げることで検出しますが、 少なくとも警告としてコンパイル時に検出したいという要望もあります。
この警告を解消するには…:
- 次のような形でdefaultを明示的に挿入:
switch (i) { case 1: dothis(); break; case 2: dothat(); break; default: assert(0); }
- return抜けの場合と同様に、 独自のエラークラスをthrowするdefault節を記述。
warning - statement is not reachable
以下のコードを考えてみましょう:
int foo(int i) { return i; return i + 1; }
2番目のreturn文は決して実行されません。言い換えると、デッドコードです。 リリース版としてはデッドコードはお奨めできたものではありませんが、 it can バグの原因を早く特定しようとしているときや 違うコード片を何種類か試しているときなど、 頻繁に発生するものでもあります。
この警告を解消するには…:
- /+ ... +/ コメントでコメントアウト:
int foo(int i) { return i; /+ return i + 1; +/ }
- version(none) の中に入れる:
int foo(int i) { return i; version (none) { return i + 1; } }
- releaseビルドのときのみ警告付きでコンパイルする。