はじめに
C言語から派生したオブジェクト指向プログラミング言語であるC++は、21世紀に入ってまったく別物とも言えるプログラミング言語に成長していきました。それは、Modern C++と称されています。1990年代にC++を触っていたプログラマが現在の仕様を知れば、隔世感に苛まれるのではないでしょうか。本連載では、かつてはC++をたしなんでいたという方、今からC++言語を始めるという方に向けて、Modern C++らしい言語仕様をピックアップし紹介していくことで、今のC++言語の姿を理解していただきます。
対象読者
- かつてはC++をたしなんでいたという方
- 今からC++言語を始めるという方
- モダンなプログラミング言語のパラダイムに興味のある方
必要な環境
本記事のサンプルコードは、以下の環境で動作を確認しています。
-
macOS Monterey/Windows 10(64bit)
- Xcode Command Line Tools 2395
- MinGW GCC 9.2.0
型推論って何?
少々間が空いてしまいましたが、20世紀のC++プログラマ(筆者)が、今(21世紀)のC++を知って驚く連載の第2回です。今回は、型推論(Type Inference)です。第1回では、「データ型は明示しないで!」と、C言語から慣れ親しんだ構文を一気に否定するようなことが書かれていましたが、それを納得感のあるように紹介するのが今回の目的です。
なぜ静的な型付けが必要なのか
改めて言うまでもなく、C++は静的な型付けを行うプログラミング言語です。つまり、コンパイルの時点で型が明確である必要があります。静的型付けのプログラミング言語では、コンパイルによって実行環境ネイティブなコード(要するに機械語かそれに準ずるもの)を生成する必要があるので、データの型すなわちメモリ上に占める領域の大きさと、それを取り扱う命令は明らかである必要があります。このようなことで、「int a;」と書いて32ビットのメモリ領域を必要とする、符号付き整数の命令を使う、などと決めるわけです。
何度も書くと間違いの元となる
「int a;」のような単純な宣言文なら、何度書こうがさして苦にはなりません。しかし、後述するテンプレート機能を使ったライブラリでは、異なる名前空間にあったり型パラメータを伴ったりするので、型名が長くなりがちで、さらにこういう型に限って、ソースコード中に何度も現れたりするものです。例えば、リスト的なデータ構造であるベクタ(std::vector)ではこんな感じです。
std::vector<int> vec{1, 2, 3}; (1) for(std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); itr++) { (2) cout << *itr << endl; }
このように、(1)(2)と「vector<int>」を2回も書かなければなりません。特に(2)では、右辺の型は分かっているんだから、左辺はそれに合わせてよ! という文句が出そうです。
初期化漏れを防止する
このように、同じ意味の型を何回も書くのはかったるいな! というときに型推論は役立つわけですが、もっと切実な理由があります。それは、変数の初期化漏れの防止です。JavaでもRustでもそうなんですが、変数は宣言しっぱなしでOKです。宣言しっぱなしというのは、明確な初期化が行われていないという意味です。こうなると、その変数を参照しようとするといろいろとまずいことが起きてきます。
- 基本データ型とかなら、どんな値か分からない
- ポインタ型とかなら、どこを指しているか分からない(ダングリングポインタ)
なので、実行時にプログラムが不可解な動作をしたり、いきなり落ちたりするわけです。しかし、コンパイラはどんどん進化しているので、初期化されていない変数を参照しようとすると警告を発したり、Rustのようにエラーとしてしまうこともできます。このように初期化漏れによる事故は限りなく低くなっていますが、できればゼロにしたいところです。
型推論の登場
そのようなわけで型推論です。型推論では、推論の根拠となるもの、ほとんどの場合は初期化子を解析し、適当と思われるデータ型を自動的に割り当てます。
auto x = 10;
何とautoです。どこかで見覚えがあるような……と思った方は、次の[NOTE]をご覧になってください。自動を意味するautoを指定することで、型はよくわからないけど、とにかく変数を宣言するぜ! というわけです。それにしてもautoかぁ、と思ったのは筆者だけでしょうか。C#やJavaなんかも今では型推論を使えるようになっていて、var(変数の意)で始めているのになぁ……(Rustなんかではletを使っているようですけど)。JavaScriptっぽく見えるのを嫌ったのでしょうか。
それはさておき型推論では、根拠すなわち初期化式が必要なので、初期化漏れを防ぐことができます。上記を「auto x;」とだけ書いても、初期化式がないので型を推論する根拠がありません。つまり宣言しっぱなしを防ぐことができます。これはつまり、プログラムの安全性が向上するということです。
このようなメリットを感じていただけたところで、詳細に切り込んでいきましょう。ただし、一口に型推論と言っても仕様は膨大なので、基本的な部分を取り上げています。「型推論」ってこんな感じなんだ、というところを感じ取ってくださいね。
[NOTE]auto宣言子
auto宣言子は、C++11で型推論に使われるようになるまでは、自動変数の宣言に使われていました。自動変数とは、スタック上に確保される変数のことで、いわゆるローカル変数、局所変数と呼ばれるものが該当します。「auto int a;」のように宣言します。autoは既定であり、実際には自動変数の宣言にautoが用いられることはほとんどなく、単に「int a;」というようにautoは省略されていました。筆者も、ほとんどautoを書いたことはありません。
ちなみに、auto宣言子はレジスタ変数のためのregister宣言子に対するものですが、register宣言子もコンパイラの最適化技術の向上とともに用いられなくなりました。優先してレジスタに割り当てるべき変数を、コンパイラが自動的に決定するからです。
[NOTE]C++プログラムのコンパイル
以降のサンプルは、基本的に「gcc -o 出力先ファイル名 ソースファイル名」でコンパイルできますが、環境によってはライブラリやC++バージョンの指定が必要になることがあります。コンパイルやリンクでエラーが発生するときには、以下のようにライブラリの指定(-lstdc++)、C++バージョンの指定(-std=c++11)の追加を検討してください。
% gcc -o sample sample.cpp -lstdc++ -std=c++11 // c++14, c++17, c++20など