null安全でない言語は、もはやレガシー言語だ、という記事がバズってましたよね。比較的新参の言語と現在のメインストリームに位置する言語を比較する過激なタイトルで、ユーザー数の多い言語に喧嘩を売った結果、内容はどうあれ反発心が形成され、不毛な議論が巻き起こったと私は認識しています。よくある悲しい話ですね。はい。
ご存知の通り(かどうかは知らないけれど)、私はC++erであり、C++は件の記事でレガシー言語として槍玉に挙げられている一つです。
個人的には、C++はレガシーと言われても仕方ない、と考えています。そもそも、C++のポインタには、null以外にも、
-
reinterpret_cast
またはCスタイルキャストによる即値の代入 - dangling pointer
- 未初期化メモリ
- 多重delete
- delete忘れによるメモリリーク
など様々な問題があり、控えめに言ってクソです。
nullポインタの厄などC++の抱える問題点の中では氷山の一角に過ぎない
— 白山風露 (@kazatsuyu) 2016年11月10日
じゃあ何でそんな言語使ってるんだ、とか言い出すと宗教戦争になり、どちらかが滅びるまで終わりません。やめましょう。
// この記事の本題とは離れますが、言語disはその言語に関わっていない人がやるとただの悪口だと私は思っています。まあdisだから悪口なんですが、言われた方はたまったものじゃない。別にツイッターで悪口言うくらいならいいですが、記事にするならちょっと考えなきゃいけないことかなと。
// 炎上芸に関しては、良し悪しを判断できるほどリテラシーを持っていないので、まあ考えたうえで過激な釣りタイトル付けてdis記事を上げるなら、各人の好きにすればいいんじゃないでしょうかね。
さて、C++は主要な実装がコンパイラーであり、したがって私も常々コンパイラーのエラー検出の恩恵に預っています。新たに追加したコードが一発でコンパイル通ることはまずありません。
ですので、「null安全性によってコンパイル時にエラーチェック漏れが判明する」というのが非常に便利だというのはよく分かるのです。
私はconstexpr教徒なので、コンパイル時にできることはなるべくコンパイル時にやるべきだと思います。
ではなぜこの記事を書いているかと言うと、冒頭に示した記事と、以下の記事を読んだ時から、どうしてももやもや感があったためです。
こちらの記事は、冒頭の記事に対する反応に対する返答として書かれた記事のようですが、その中に
null安全にデメリットはないし、生産性も低下しません。
という文言がありました。
本当にそうだろうか?
null安全には、本当に、全く、欠片も、1クロックの処理時間さえも、デメリットがないのだろうか?
これに関してもやもやと考えていたのですが、結局のところ、「重箱の隅のようなケースで、処理速度にデメリットが存在するだろう」という結論に至りました。
もちろん、ここで言うデメリットは、「本来nullチェックをしなければならない箇所に、nullチェックを追加することによる処理時間の増大」を指す訳ではありません。それはただのバグ修正です。そうではなく、「本来nullチェックが不要だったはずの箇所に、nullチェックを入れなければならない」ことによるデメリットです。
null安全性という概念を拡張すると、配列の範囲安全性という考え方が現れる、と私は考えています。
null安全性が言語機能レベルで実装可能なのに対し、配列の範囲安全性は言語機能レベルではなかなか難しいように思いますが。
配列の範囲安全性という考え方では、nullチェックはインデックスの範囲チェックに該当します。nullableな型は、長さが0または1の配列と同等の性質を持つように思います。
配列のアクセスで毎回範囲チェックを行うのは、明らかにオーバーヘッドがあります。C++では、その問題を解決するために、「チェックを行わないが範囲外のインデックスを渡すと未定義動作になる」というアクセス方法と、「毎回チェックを行い、範囲外のインデックスに対しては例外を発生させる」というアクセス方法を用意して、ユーザーに選択させるようにしています。
たとえば、配列の3番目の要素と、5番目の要素にアクセスしたい、という場合を考えてみます。
この場合、配列の長さが5以上であれば、範囲チェックは1回で済みます。3番目の要素は、5番目の要素が存在している時点で必ず存在するからです。
この手の依存関係をコンパイラーが正確に把握することは困難です。少なくとも、現状では不可能でしょう。
nullableな型に戻ってみます。nullableな型は、長さが0または1の配列と同等だ、と述べました。
この考え方では、依存関係は存在しません。ただし、これはnullableな変数が1つだけの場合です。
nullableな変数が2つ以上存在し、それぞれの初期化状態に依存関係があるパターンというのが存在するのではないでしょうか。
以下のようなクラスを考えます。(C++1z形式なので、動く保証はありません)
class X {
std::optional<int> a;
std::optional<int> b;
public:
X() = default;
X(std::optional<int> v) : a(v) {}
void initialize() {
if(a) {
b = *a * 2;
}
}
friend std::ostream& operator << (std::ostream &out, const X& rhs) {
if(rhs.b) {
out << *rhs.a << "," << *rhs.b << std::endl;
}
return out;
}
};
このクラスでは、b
の初期化状態はa
の初期化状態に依存します。b
はa
が有効な値でなければ有効になりません。
逆に、有効性に関しては、b
が有効であれば、a
は必ず有効であるという依存関係が現れます。
しかし、コンパイラーにはこの依存性を見抜くのが困難です。もしC++がnull安全な言語であれば、operator <<
はこう書かなければなりません。
friend std::ostream& operator << (std::ostream &out, const X& rhs) {
if(rhs.a && rhs.b) {
out << *rhs.a << "," << *rhs.b << std::endl;
}
return out;
}
もちろん、この程度のオーバーヘッドは、ほとんどの場合で問題とされないでしょう。
早すぎる最適化だと言われればその通りだと思います。
将来的な仕様変更を見越して、安全側に倒すためにnullチェックを追加しておいた方がいいかもしれません。
繰り返しますが、これは重箱の隅です。重箱の隅ですが、null安全性を言語仕様に追加するのは全くのゼロコストではないよ、という反例を挙げさせていただきました。
追記
コメントでも指摘を頂きましたが、null安全言語は!を使用してnullチェックを行わない(言わばレガシーな)アクセスが可能な設計になっているそうです。
もちろん、この方法はnull安全言語の恩恵を捨てることになるので乱用すべきではありませんが、論理的にnon-nullであることが保証できるなら使用しても良い、ということですね
追記その2
さらにコメントで指摘を頂きましたが、Swiftでは!は-Ouncheckedオプションをつけない限り例外を投げるそうです。これをコストと捉えることができるかもしれませんが、そもそもC/C++ではnull参照が未定義動作になっていますが、他の多くの言語では例外を発生させることになっているので、null安全であることに起因するコストではないでしょう