標準C++[C++,1998]を最大限に活用するために、我々はC++プログラムの 書き方をもう一度よく考えてみる必要がある。そのような「再考」の取り組み として、C++の学び方(と教え方)について考えてみる。我々はどのような 設計テクニックとプログラミングテクニックを重視したいのだろうか。 言語のどの部分を最初に学びたいのだろうか。実際のコードの中で言語の どの部分に重点を置きたいのだろうか。
本稿では、標準ライブラリを使用して最新のスタイルで書かれた 簡単なC++プログラムをいくつか例に上げ、それらを従来のCスタイルで 書かれたものと比較する。そして、そのような簡単な例から得られた教訓が、 規模の大きなプログラムにも当てはまることを簡潔に示す。さらに、低レベルな スタイルと比べても効率を下げることなく簡潔さをもたらす、抽象化を用いた 高級言語としてC++を使用するよう主張する。
プログラムは書きやすく、正確で、保守可能であり、受け入れられる程度に 効率的であることが望ましい。したがって、この理想に最大限に近づけるように C++(そして、その他のプログラミング言語)を使用すべきである。私が思うに、 C++コミュニティは標準C++が提供する機能をまだ自分のものにしていない。 それゆえに、C++の使い方を再考することで、その理想に近づくように大きく 改善できるであろうと考える。本稿では、標準C++が提供する機能そのもの ではなく、機能がサポートするプログラミングスタイルに着目する。
ライブラリを使用してコード量を減らし、複雑さを抑えることが大きな 改善への鍵となる。本章以降で、C++入門講座で見られるような簡単な例を いくつか用いてそれらを実演し、削減量を示す。
コード量を減らし、複雑さを抑えることで、我々は開発時間を短縮し、 保守を容易にし、テストのコストを下げる。さらに重要なことには、C++の 習得も容易にする。このような簡略化は、小さなプログラムや選択講座で 良い成績を得ようとするだけの学生には十分なものだろう。しかし、プロの プログラマにとって、効率は重大な問題である。我々のプログラミング スタイルは、効率が犠牲にならなければ、最新のサービスやビジネスが 日々直面する多量のデータやリアルタイム要求を処理するシステムで 使用できるようになると考えられる。そこで、効率を下げることなく 複雑さを抑えられるということをはっきりと示す測定結果を示す。
最後に、C++の学習法と指導法に関するこのような考えの意味する ものについて論じる。
プログラミング言語の二つ目の課題によくみられる、次のような プログラムを考えてみる。
プロンプト"Please enter your first name"を出力する 名前を読み込む "Hello <名前>"と出力する
標準C++では、次のようになるはずだ。
#include<iostream> // 標準入出力機能を利用する #include<string> // 標準文字列を利用する int main() { using namespace std; // 標準ライブラリへアクセスする cout << "Please enter your first name:\n"; string name; cin >> name; cout << "Hello " << name << '\n'; }
まったくの初心者には、main()とは何か、#includeは 何を意味するのか、usingは何をするものなのか、といった「足場」を 説明する必要がある。さらに、 \n は何をするものなのか、セミコロンは どこに置けばよいのか、といった「細かな」約束事もすべて理解する必要がある。
しかし、プログラムの主要な部分は概念的に単純で、問題文とは表記が 異なるだけである。我々は記法を学ばねばならないが、それは比較的簡単な ことだ。stringは文字列であり、coutは出力で あり、 << は出力へ書き込む演算子である。
比較のために、従来のCスタイルによる方法を考えてみる。 [*1]
#include<stdio.h> // 標準入出力機能を利用する int main() { const int max = 20; // 名前の最大長は19文字 char name[max]; printf("Please enter your first name:\n"); scanf("%s", name); // 複数の文字をnameに読み込む printf("Hello %s\n", name); return 0; }
配列や呪文のような %s について説明しなければならないため、 客観的に見ても、このコードの中心となるロジックはC++スタイル版よりも わずかに(しかし、ほんの少しだけ)複雑になっている。問題なのは、この単純な Cスタイルによる方法には手抜きがあるということだ。もし、"first name"に マジックナンバーの19(ゼロで終端するCスタイル文字列のために、定数20から 1を引いた数)よりも長い文字列を入力したら、このプログラムに悪影響を与える ことになる。
この種の手抜きは「後で」適切な方法にするのであれば、害はないと言える。 しかし、それはせいぜい「許容できる」程度のものであって、「十分」なもの とは言えない。初心者にはこのような脆弱なプログラムを示さないのが理想的だ。
前掲したC++スタイルのプログラムとある程度同じ振る舞いをするCスタイルの プログラムはどのようなものになるだろうか。まず、scanf()をより 適切な方法で使用すれば、配列のオーバーフローを防ぐことができるだろう。
#include<stdio.h> // 標準入出力機能を利用する int main() { const int max = 20; char name[max]; printf("Please enter your first name:\n"); scanf("%19s", name); // nameに最大19文字読み込む printf("Hello %s\n", name); return 0; }
scanf()の書式指定文字列の中で、バッファサイズを表すシンボル 形式のmaxを直接使用する標準的な方法はないため、数値リテラルを 使用しなければならなかった。これは良いスタイルとは言えず、メインテナンスの 障害になる。次に示す熟練者レベルの方法は、初心者に説明するのは避けたい。
char fmt[10]; sprintf(fmt, "%%%ds", max-1); // 書式指定文字列の作成: %sだけではオーバーフローの可能性がある scanf(fmt, name); // nameに最大max-1文字読み込む
その上、このプログラムは「余分」な文字を捨ててしまう。我々が欲しいのは、 入力に応じて伸長する文字列だ。そうするには、抽象度を下げ、個々の文字を扱う ようにしなければならない。
#include<stdio.h> #include<ctype.h> #include<stdlib.h> void quit() // エラーメッセージを出力し、終了する { fprintf(stderr, "memory exhausted\n"); exit(1); } int main() { int max = 20; char* name = (char*)malloc(max); // バッファを割り当てる if (name == 0) quit(); printf("Please enter your first name:\n"); while (true) { // 先行する空白文字をスキップする int c = getchar(); if (c == EOF) break; // ファイル終端 if (!isspace(c)) { ungetc(c, stdin); break; } } int i = 0; while (true) { int c = getchar(); if (c == '\n' || c == EOF) { // 入力終了: 終端のゼロを追加する name[i] = 0; break; } name[i] = c; if (i == max - 1) { // バッファが一杯 max = max + max; name = (char*)realloc(name, max); // さらに大きな新しいバッファを獲得する if (name == 0) quit(); } i++; } printf("Hello %s\n", name); free(name); // メモリを解放する return 0; }
前の版と比べると、かなり複雑に見える。元の問題文には、空白をスキップする 必要があるとは明記しなかったので、そのためのコードを追加するのは気が引けたが、 先行する空白をスキップするのは当然で、他の版のプログラムでもそうしている。
この例は、それ程悪いものではないと言える。熟練したCプログラマとC++プログラマ なら ― 実際のプログラムの中で ― おそらく(そうあってほしい?)同じような プログラムを書いたことがあるだろう。書いたことがなければ、プロのプログラマ とさえ言えないかもしれない。しかし、初心者には余計な思考の負担をかけてしまう と考えてほしい。この改良版は9種類の標準ライブラリを利用し、文字単位の入力を かなり細かい方法で扱い、ポインタを使用し、自由記憶を明示的に扱っている。 移植性を保ちながらrealloc()を使用するため、(newの 代わりに)malloc()を使用した。これは、サイズとキャストの問題を 引き起こす[*2]。 このような小さなプログラムでは、メモリ不足にどう対処するのが 最善かはっきりしない。ここでは議論がそれないように、単純明解な方法を用いた。 Cスタイルによる方法を用いる場合、どのような方法がより一層教育と普及にとって よい基盤となるか、慎重に検討しなければならない。
要約すると、最初の単純な問題を解くには、問題の解決に本質的に必要なものに 加え、ループ、テスト、領域サイズ、ポインタ、キャスト、明示的な自由記憶管理を 導入する必要があった。さらに、このスタイルはエラーの機会に満ちている。 長年にわたる経験のおかげで、私は明らかなoff-by-one(ひとつ違い)エラーや アロケーションエラーを犯すことはなかった。はじめに少しストリームI/Oを 扱ったときに、(intではなく)charに読み込み、 EOFと比較するのを忘れるという、昔から初心者がよく犯す間違いを してしまった。C++標準ライブラリのようなものがなければ、多くの指導者が 「手抜き」の方法を使用し続け、これらの問題を先送りするのは不思議なこと ではない。残念なことに、学習者の多くは、手を抜いたスタイルでも「十分」 であり、(C++スタイル以外の)他の方法よりも簡単に書けることに注目する。 その結果、直し難い癖がつき、バグだらけのコードを残していくことになる。
最後のCスタイルのプログラムは41行あり、それに比べ、同じ機能を持つC++ スタイルのプログラムは10行である。「足場」の部分を除けば、30行と4行になる。 重要なのは、C++スタイルは行数が少なく、本質的に理解しやすいということだ。 C++スタイル版とCスタイル版の説明に要する概念の数と複雑さは、客観的に 比較しにくいが、10対1の割合でC++版の方が優位ではないだろうか。
前節で示したような小さなプログラムでは、効率は問題にならない。そのような プログラムでは、簡潔さと(型)安全性が重要である。しかし、実際のシステムでは 効率が極めて重要となる部分が多くある。そのようなシステムでは、「抽象度を 上げられるか」が課題となる。
効率を重視するプログラムに見られる、次のような処理の簡単な例を 考えてみよう。
要素数が不明な要素を読み込む 各要素に対して何らかの処理をする すべての要素に対して何らかの処理をする
私が思いつく最も単純で具体的な例は、倍精度浮動小数点数の列を入力から 読み込み、その平均とメジアンを求めるプログラムだ。従来のCスタイルによる 方法は、次のようになるだろう。
// Cスタイルによる方法: #include <stdlib.h> #include <stdio.h> int compare(const void* p, const void* q) // qsort()が使用する比較関数 { register double p0 = *(double*)p; // double型の値を比較する register double q0 = *(double*)q; if (p0 > q0) return 1; if (p0 < q0) return -1; return 0; } void quit() // エラーメッセージを出力して終了 { fprintf(stderr, "memory exhausted\n"); exit(1); } int main(int argc, char* argv[]) { int res = 1000; // 最初の割り当て量 char* file = argv[2]; double* buf = (double*)malloc(sizeof(double) * res); if (buf == 0) quit(); double median = 0; double mean = 0; int n = 0; // 要素数 FILE* fin = fopen(file, "r"); // 読み込み用にファイルをオープンする double d; while (fscanf(fin, "%lg", &d) == 1) { // 数値を読み込み、移動平均を更新する if (n == res) { res += res; buf = (double*)realloc(buf, sizeof(double) * res); if (buf == 0) quit(); } buf[n++] = d; mean = (n == 1) ? d : mean + (d - mean) / n; // 丸め誤差が生じやすい } qsort(buf, n, sizeof(double), compare); if (n) { int mid = n / 2; median = (n % 2) ? buf[bid] : (buf[mid - 1] + buf[mid]) / 2; } printf("number of elements = %d, median = %g, mean = %g\n", n, median, mean); free(buf); }
比較のために、C++の慣用的な方法を以下に示す。
// 標準C++ライブラリを使用した方法 #include <vector> #include <fstream> #include <algorithm> using namespace std; int main(int argc, char* argv[]) { char* file = argv[2]; vector<double> buf; double median = 0; double mean = 0; fstream fin(file, ios::in); // 読み込み用にファイルをオープンする double d; while (fin >> d) { buf.push_back(d); mean = (buf.size() == 1) ? d : mean + (d - mean) / buf.size(); // 丸め誤差が生じやすい } sort(buf.begin(), buf.end()); if (buf.size()) { int mid = buf.size() / 2; median = (buf.size() % 2) ? buf[mid] : (buf[mid - 1] + buf[mid]) / 2; } cout << "number of elements = " << buf.size() << ", median = " << median << ", mean = " << mean << '\n'; }
前の例ほどサイズに著しい違いはみられない(空行を除いて43行と24行)。 main()の宣言やメジアンの計算のような、それ以上簡略できない 共通部分(13行)を除いて、その違いは20行と11行である。必要不可欠な入力と保存の 繰り返しとソートは、どちらもC++スタイルによるプログラムの方がかなり短くなって いる(入力-保存の繰り返しは9行と4行。ソートは9行と1行)。さらに重要なことに、 それらのロジックはC++版の方がはるかに簡潔になる。その結果、はるかに理解 しやすい。
繰り返すが、C++スタイルによるプログラムはメモリ管理を暗黙のうちに 行っている。push_back()を使用して要素を追加するとき、 vectorは必要に応じて伸長する。Cスタイルによるプログラムの方は、 realloc()を使用してメモリ管理を明示的に行っている。C++スタイルに おけるvectorのコンストラクタとpush_back()は、 基本的にCスタイルにおけるmalloc()とrealloc()や、 割り当てられたメモリ量を記録するコードと同じことをしている。C++スタイル では、メモリ不足を報告するために例外処理を用いている。Cスタイルでは、メモリを 不正に使用しないように、明示的なテストを追加した。
予想通り、C++版の方が正しいものを作りやすかった。私はこのC++スタイル版を、 Cスタイル版をカットアンドペーストして作成した。そのとき、<algorithm>の インクルードを忘れ、buf.size()を使用しないで2箇所の変数nを そのままにした。さらに、使用していたコンパイラはusingディレクティブを ローカルで使用できなかったので、main()の外に移動しなければ ならなかった。これら4個のエラーを修正した後は、プログラムははじめから 正しく動作した。
初心者にはqsort()は「奇妙」なものにみえるだろう。なぜ要素数を 与えなければならないのか(配列は要素数を知らないから)。なぜdoubleの サイズを与えなければならないのか(qsort()はdoubleを ソートすることを知らないから)。なぜdoubleを比較するあのような 醜い関数を書かなければならないのか(qsort()はソートする要素の型を 知らないため、比較関数のポインタを必要とするから)。なぜqsort()の 比較関数は、引数にchar*ではなくconst void*を受け取る のか(qsortは非文字列の値を基にソートできるから)。void* とは何か。そしてそれをconstにする意味は何か(「えー、それについては 後でふれます」)。答えの複雑さに対する驚きのあまり、初心者が呆気にとられないように これらを説明するのは容易なことではない。一方、sort(v.begin(), v.end())を 説明するのは比較的容易だ(このケースでは、sort(v)の方が さらにわかりやすいだろうが、コンテナの一部をソートしたいこともあるので、ソート したい範囲の初めと終わりを指定する方がより汎用的である)。
効率を比較するため、まず初めに効率の比較が有意となるには、どれくらいの 入力が必要であるかを測定した。50,000要素では、プログラムは0.5秒未満で終了 してしまう。そこで、500,000要素と5,000,000要素を比較することにした。
未最適化 | 最適化 | |||||
---|---|---|---|---|---|---|
C++ | C | C/C++比 | C++ | C | C/C++比 | |
500,000要素 | 3.5 | 6.1 | 1.74 | 2.5 | 5.1 | 2.04 |
5,000,000要素 | 38.4 | 172.6 | 4.49 | 27.4 | 126.6 | 4.62 |
重要な数値は比率である。1より大きな数値は、C++スタイル版の方が速いことを 意味する。言語、ライブラリ、プログラミングスタイルの比較は難しいことで有名だ。 だから、このような単純なテストの結果で大胆な結論を出さないでほしい。数値は 他の静かな計算機上で何度か試行した平均値である。各試行で得られた数値の差は 1%未満であった。さらに、ISO Cに厳格に適合させたCスタイルプログラムでも 実行してみた。予想通り、C++と同等の機能を持つCスタイルプログラムとの 性能の違いはみられなかった。
私はC++スタイルのプログラムの方がほんの少しだけ速くなると予想していた。 他のC++実装を調べてみて、驚くべき差異があることがわかった。データ量が 少ない場合は、Cスタイル版の方がC++スタイル版よりも性能が上回ることもある。 しかし、この例で重要なのは、現在の技術でも抽象度をさらに高くして、 エラーをより一層防ぐことが可能であるということだ。私が使用した実装は、 広く利用でき、安価なものである。調査のためだけに用意されたものではない。 さらに性能が高いとされる実装も利用できる。
利便性を得るためやエラーを防止するためには、3倍や10倍、ときには50倍の 出費も厭わないと思う人達がいることは珍しくない。しかも、速度が2倍や4倍に なるのは目を見張るものがある。これらの値は、C++ライブラリベンダが満足する 最低限のものであるべきだ。
どこで時間を費しているかよく知るために、さらにいくつかテストしてみた。
未最適化 | 最適化 | |||||
---|---|---|---|---|---|---|
C++ | C | C/C++比 | C++ | C | C/C++比 | |
読み込み | 2.1 | 2.8 | 1.33 | 2.0 | 2.8 | 1.40 |
生成 | .6 | .3 | .5 | .4 | .3 | .75 |
読み込みとソート | 3.5 | 6.1 | 1.75 | 2.5 | 5.1 | 2.04 |
生成とソート | 2.0 | 3.5 | 1.75 | .9 | 2.6 | 2.89 |
「読み込み」は単にデータを読み込み、「読み込みとソート」はデータを 読み込んでそれをソートするが、出力はしない。「生成」は、入力にかかる コストをよく知るために、データを読み込む代わりにランダムな数値を生成する。
未最適化 | 最適化 | |||||
---|---|---|---|---|---|---|
C++ | C | C/C++比 | C++ | C | C/C++比 | |
読み込み | 21.5 | 29.1 | 1.35 | 21.3 | 28.6 | 1.34 |
生成 | 7.2 | 4.1 | .57 | 5.2 | 3.6 | .69 |
読み込みとソート | 38.4 | 172.6 | 4.49 | 27.4 | 126.6 | 4.62 |
生成とソート | 24.4 | 147.1 | 6.03 | 11.3 | 100.6 | 8.90 |
他の例や実装から予想するに、streamioの方がstdioよりも 多少遅くなるのではないかと予想していた。filestreamではなく、 cinを使用していたこのプログラムの前の版が、実はこのケースにあたる ものだった。C++実装の中には、ファイルI/Oの方がcinよりもはるかに 速いものもあるようだ。それは、少なくともcinとcoutの間の 連携が一部貧弱な取り扱いをしているからである。しかし、これらの数値は C++スタイルのI/OがCスタイルのものと同じくらい効率がよくなる可能性がある ということを示している。
浮動小数点数の代わりに整数を読み込むようにプログラムを変更しても、 性能に変化はみられなかった ― C++スタイルプログラムの方がはるかに 変更しやすかったけれども(Cスタイルの12箇所に比べ、C++スタイルは2箇所)。 それはメインテナンス上、良い兆しだ。
「生成」テストにみられる差異は、アロケーションにかかるコストの差に よるものだ。vectorとpush_back()は、配列とmalloc() /free()と同じくらい速くなるはずであるが、そうはならなかった。 それは、何もしない初期化呼び出しの最適化に失敗したからのようだ。 幸い、アロケーションのコストは、アロケーションの必要性の原因となる 入力のコストに比べると(常に)小さく見える。
予想通り、sort()はqsortに比べ、著しく高速であった。 その主な理由は、qsort()は関数を呼び出す必要があるが、sort()は 比較演算がインライン化されるからである。
効率の問題を説明する例を選ぶのは難しい。数値の読み込みと比較は 現実的なものではないというコメントを私の同僚からもらった。そこで、 文字列の読み込みとソートをすべきだと思い、次のプログラムを試してみた。
#include <vector> #include <fstream> #include <algorithm> #include <string> using namespace std; int main(int argc, char* argv[]) { char* file = argv[2]; // 入力ファイル名 char* ofile = argv[3]; // 出力ファイル名 vector<string> buf; fstream fin(file, ios::in); string d; while (getline(fin, d)) buf.push_back(d); // 読み込んだ行をbufへ追加する sort(buf.begin(), buf.end()); fstream fout(ofile, ios::out); copy(buf.begin(), buf.end(), osteram_iterator<string>(fout "\n")); // 出力へコピーする }
私はこのコードをCへ書き換え、文字の読み込みを少し最適化してみた。 C++スタイルのコードは、文字列のコピーを除去した手作業で最適化した Cスタイルのコードと比べても、よい結果を出す。少量の出力では大きな 違いはみられず、多量のデータでは、インライン化によりsort()は 再びqsort()に優る。
C++ | C | C/C++比 | C (文字列コピーなし) | C/C++比 (最適化) | |
---|---|---|---|---|---|
500,000要素 | 8.4 | 9.5 | 1.13 | 8.3 | .99 |
2,000,000要素 | 37.4 | 81.3 | 2.17 | 76.1 | 2.03 |
文字列数を2,000,000個にしたのは、ページングすることなく5,000,000個の 文字列を扱える十分な量のメインメモリがなかったからである。
さらに、どこで時間を消費しているか調べるために、sort()を除いたプログラムも 試してみた。
C++ | C | C/C++比 | C (文字列コピーなし) | C/C++比 (最適化) | |
---|---|---|---|---|---|
500,000要素 | 2.5 | 3.0 | 1.20 | 2.0 | .80 |
2,000,000要素 | 9.8 | 12.6 | 1.29 | 8.9 | .91 |
文字列は比較的短いもの(平均7文字)を使用した。
stringはたまたま標準ライブラリに含まれている、完全に通常の ユーザ定義型である。stringに対して効率的で簡潔にできることは、 他の多くのユーザ定義型に対しても行うことができる。
私がプログラミングスタイルと指導の文脈で効率について論じるのはなぜか。 我々が教えるスタイルとテクニックは、現実世界の問題にスケールしなければ ならない。C++は ― 特に ― 大規模システムと効率に制約のあるシステムを 対象にしたものである。したがって、小さなプログラムにだけ効果のある スタイルとテクニックを使用するようにC++を教えるのは、受け入れられない。 そのような方法で指導された人達は失敗し、教えられたことをあきらめてしまう ようになるだろう。シンプルで型安全なコードを提供するためにジェネリック プログラミングと具象型に大きく依存するC++スタイルは、従来のCスタイルに 比べて効率的になることを上記の測定結果は示している。同様の結果が オブジェクト指向スタイルからも得られた。
標準ライブラリの実装によって性能が劇的に異なるのは、深刻な問題である。 標準ライブラリ(あるいは標準に含まれていない広く配布されているライブラリ)を 使用したいプログラマにとって、あるシステム上で良い性能を発揮するプログラミング スタイルが、他のシステム上で少なくとも受け入れられる程度に性能を発揮する ことは重要なことが多い。私のテストプログラムがあるシステム上で、C++スタイルが Cスタイルに比べて2倍速く実行され、他のシステム上では半分の速さでしか なかったのには本当に驚いた。プログラマがシステム間で4倍のばらつきがある ものを受け入れなければならないのはおかしい。私が知る限り、結果にばらつきが あるのは、根本的な原因によるものではない。したがって、ライブラリ実装者の 英雄的な努力なしに達成できるはずだ。よく最適化されたライブラリは、標準C++の 目に見える性能と実際の性能の両方を改善する、最も簡単な方法かもしれない。 コンパイラ実装者は、他のコンパイラと比較される小さな性能の低下をなくす ために精を出す。標準ライブラリの実装の中の方が改善する余地は大きいと思う。
上記のC++スタイルによる方法がCスタイルに比べて簡潔になったのは、明らかに 標準ライブラリによるものである。この比較は非現実的で不公平なものだろうか。 私はそうは思わない。C++の重要な側面のひとつは、簡潔で効率的なライブラリを サポートする能力だ。簡単な例で示された利点は、簡潔で効率的なライブラリが 存在するか、存在する可能性のあるあらゆるアプリケーション分野で有効である。 C++コミュニティの課題は、普通のプログラマがこういった恩恵を得られる分野を 広げることだ。すなわち、我々はさらに多くのアプリケーション分野のために、 簡潔で効率的なライブラリを設計、実装し、広く利用できるようにしなければ ならない。
プロのプログラマでも、はじめにプログラミング言語を完全に習得してから 使用するのは不可能だ。プログラミング言語は、その機能を小さな例の中で実際に 使用するなどして習得するものである。したがって、我々は常に言語のサブセットを 次々と習得していくことによって学習する。本当の問題は「はじめにサブセットを 学ぶべきか」ではなく、「どのサブセットをはじめに学ぶべきか」である。
「どのサブセットをはじめに学ぶべきか」という問いに対する従来の答えは、 「C++のCサブセット」というものであった。よく考えてみたが、これはよい答え とは言えない。Cを最初に学ぶという方法は、早い段階で低水準な詳細に集中して しまうことになる。さらに、学習者が関心のあることを表現するのに多くの技術的な 問題に直面せざるを得なくなることで、プログラミングスタイルと設計上の問題を わかりにくくする。2節と3節で取り上げた例が、それを示している。C++の優れた ライブラリサポート、記法のサポート、型検査は、「Cを最初に学ぶ」という方法に 比べ、明確な結果をもたらす。しかし、私が勧めるのは「純粋なオブジェクト指向を 最初に学ぶ」という方法ではない。それはまったく違うと思う。
プログラミング初心者に対しては、C++の学習は効果的なプログラミング テクニックの学習を支援すべきである。C++は初めてというベテランプログラマに 対しては、効果的なプログラミングテクニックをC++でどう表現するかや、まだ 知らないテクニックの学習に集中した方がよい。経験豊富なプログラマが陥りやすい 最大の落し穴は、他の言語で効果的だったものをC++で表現してしまいやすく なることだ。初心者と経験豊富なプログラマのどちらも、概念とテクニックに 重点を置くべきである。C++の構文とセマンティクスの詳細は、C++がサポートする 設計テクニックとプログラミングテクニックの理解には、あまり重要ではない。
指導は厳選された具体的な例からはじめ、より一般的で抽象的なものへと 進めていくべきである。これは子供が物事を学ぶときの方法であり、私たちの 多くが新しい概念を理解するときの方法だ。言語機能は常にそれらが使用される 文脈で示されるべきである。そうでなければ、プログラマの関心がシステムの 構築から技術的に難解なものを楽しむことへと移ってしまう。言語の技術的な 詳細に注目するのは楽しいことではあるが、効果的な教育にはならない。
一方、プログラミングを単に分析と設計に付随するものとみなすのもうまく いかない。高水準で技術的なトピックがすべて完全に示されるまでコードに 関する議論を後回しにする方法は、多くの人にとって損失の大きな失敗であった。 そのような方法は人々をプログラミングから遠ざけ、製品品質のコードの開発に おける知的な挑戦を著しく軽視することになる。
「設計を最初に行う」という方法の対極にあるのは、C++の実装を入手して コードを書き始めるというものだ。問題に遭遇したときには、マウスを クリックしてオンラインヘルプを参照する。この方法の問題点は、個々の機能の 理解に完全に偏ってしまうことだ。このような方法では、一般的な概念と テクニックを学ぶのは難しい。また、経験豊富なプログラマの場合、C++の文法と ライブラリ関数を用いながらも、これまでに使用していた言語で考えようとする 傾向に拍車をかけてしまうという問題もある。初心者の場合は、ベンダの提供する コード例からカットアンドペーストして挿入したコード片が混ざりあった、 if-then-elseだらけのコードになる。そのようなコードの目的はしばしば初心者には わかりにくいものとなり、効果を得る方法がまったく理解できない。これは、 賢い人にも言える。このような「探しまわる方法」は、効果的な指導や信頼できる 教材の補助として最も有用になり得るが、それだけでは災いの元となる。
まとめると、私が勧めるのは次のような方法だ。
私はこれが特に奇抜で画期的なものだとは思わない。普通、これは常識だと思う。 しかし、C++の前にCを学ぶべきか、オブジェクト指向プログラミングを本当に 理解するにはSmalltalkを使用しなければならないか、純粋なオブジェクト指向の 流儀(それが何を意味するにせよ)でプログラミングを学び始めなければならないか、 コードを書き始める前にソフトウェア開発プロセスを完全に理解しておかなければ ならないか、といったような具体的なトピックに関する白熱した議論の前では、 常識を見失いがちになる。
幸い、私の条件を満す方法を用いたことが少しある。私が気に入っている 方法は、良いライブラリとともに、変数、宣言、ループといった基本的な言語の 概念から教えるというものだ。学習者をプログラミングに集中させるには、 Cスタイル文字列のような複雑なものではなく、ライブラリが不可欠だ。 私はC++標準ライブラリか、そのサブセットを使用することを勧める。これは、 米国の高校で教えられるComputer Science Advanced Placementコースで 採用されている方法だ[Horwitz,1999]。経験豊富なプログラマを対象とした、 さらに上級者向けの方法も成功している。その例は[Koenig,1998]を 参照していただきたい。
このような具体的な方法の弱点は、初期の段階でシンプルなグラフィクスと グラフィカル・ユーザ・インタフェイスがないことである。これは、商用ライブラリへの 非常にシンプルなインタフェイスで補うことが(容易に?)できるだろう。 「非常にシンプル」とは、学習者がC++講座の2日目に使える程度のものという意味だ。 しかし、そのようなシンプルなグラフィクスとグラフィカル・ユーザ・インタフェイスの C++ライブラリは、広く利用できるようにはなっていない。
最初にライブラリを用いて学習/指導した後は、学習者のニーズと関心に応じて、 さまざまな方法で講義を進めることができる。ある時点で、C++の複雑で低水準な 機能について触れなければならないときがくる。ポインタ、キャスト、メモリ割り当て などを学習/指導するひとつの方法は、基礎を学ぶときに使用したクラスの実装を 調べることだ。たとえば、stringクラス、vectorクラス、listクラスの実装は、 講義の初期の段階では省略しておいた方がよいC++のCサブセットが持つ言語機能を 論じるのに最も適している。
vectorやstringのような可変データを扱うクラスを実装するには、自由記憶と ポインタが必要になる。そのようなクラスを教える前に、Date型、Point型、Complex型 といった、自由記憶とポインタを必要としないクラス(具象クラス)が、クラス実装の 基礎を教えるのに利用できる。
私はコンテナとその実装について論じた後に、抽象クラスとクラス階層を 紹介することが多いが、他に方法はいくらでもある。実際にどの順序でトピックを 取り上げるかは、使用するライブラリにより決めるべきだ。たとえば、クラス階層に 依存するグラフィクスライブラリを使用する講座では、比較的早い段階で ポリモフィズムの基礎と導出クラスの定義について説明する必要があるだろう。
最後に、C++とそれに関連する設計、プログラミングテクニックの正しい 学び方と教え方には、さまざまな方法があるということを忘れないでほしい。 学習者の目的や背景はそれぞれ異なり、それと同じように指導者と教科書の 著者の背景や経験も異なる。
プログラムは書きやすく、正確で、保守可能であり、受け入れられる程度に 効率的であることが望ましい。そうするためには、Cと初期のC++で一般的に 行われてきたものよりも、抽象度の高い設計とプログラミングをする必要がある。 ライブラリを使用することで、低水準なスタイルと比較しても効率を下げる ことなく、この理想を達成できる。したがって、さらに多くのライブラリを 作成し、(標準ライブラリのような)多くの人が利用するライブラリの実装に より一層一貫性を持たせ、広く利用できるようにすることで、C++コミュニティに 大きな利益をもたらすことができる。
簡潔で高水準なプログラミングスタイルへの流れの中で、教育は大きな役割を 果さなければならない。効率の悪さに対する根拠のない恐れから、言語やライブラリの 最も低水準の機能をはじめから利用するプログラマの世代をC++コミュニティは 必要としない。C++の初心者だけでなく、経験豊かなC++プログラマも、標準C++を 新しい高水準な言語として使えるようにしなければならないのはもちろんのこと、 どうしても必要なときにだけ抽象度を下げるようにしなければならない。標準C++を 単にCとしてやクラスを持つC(C with Classes)として利用するだけでは、標準C++が 提供する機会を逃すことになるだろう。
標準C++の学習について論文を書くことを提案してくれたChunk Allisonに感謝する。 Andrew KoenigとMike Yangには、初期の草稿に対して建設的なコメントをくれたことに 感謝する。本稿で使用されたコード例は、Cygnus EGCS1.1でコンパイルし、Sun Ultrasparc 10上で動作確認した。使用したプログラムは、私のホームページ http://www.research.att.com/~bs から入手できる。
[C++,1998] | X3 Secretariat: Standard - The C++ Language. ISO/IEC 14882:1998(E). Information Technology Council (NCITS). Washington, DC, USA. (See http://www.ncits.org/cplusplus.htm). |
[Horwitz,1999] | Susan Horwitz: Addison-Wesley's Review for the Computer Science AP Exam in C++. Addison-Wesley. 1999. ISBN 0-201-35755-0. |
[Koenig,1998] | Andrew Koenig and Barbara Moo: Teaching Standard C++. (part 1,2,3,and 4) Journal of Object-Oriented Programming, Vol 11 (8,9) 1998 and Vol 12 (1,2) 1999. |
[Stroustrup,1997] | Bjarne Stroustrup: The C++ Programming Language (Third Edition). Addison-Wesley. 1997. ISBN 0-201-88954-4. |
[1] | 私は美的な理由でC++スタイルのシンボル定数と // 形式の コメントを使用している。ISO Cプログラムに厳格に適合させるには、 #defineと /* */ 形式のコメントを使用すること。 |
[2] | Cでは明示的にキャストしなくても書けることは知っている。しかし、それは void*から任意のポインタ型への危険な暗黙の変換を認めるという 犠牲を払っている。したがって、C++はキャストを必要とする。 |
この文書は原著者の許可を得て翻訳、公開しています。
著者: | Bjarne Stroustrup |
---|---|
原文: | Learning Standard C++ as a New Language The C/C++ Users Journal, May 1999 |
日本語訳: | 神宮信太郎 (jin@libjingu.jp) |
日本語訳に関するコメント、誤りの指摘などありましたら、訳者までお知らせください。