C言語が難しいのか、それともCプログラミングが難しいのか?

問い:C言語は難しいか?
答え:C言語の言語仕様自体は難しくないが、C言語でのプログラミングは難しい(私見)。

C言語が簡単なのか難しいのか問われた場合、C言語の「なに」の難易度について問われているのか考えなくてはならない。

C言語そのものは難しくない

C言語の言語本体や標準ライブラリ部分──すなわちC言語という「抽象化された計算モデル」とでもいうべき部分については、さほど難しくない。

C言語の仕様はシンプルだ。C言語オブジェクト指向プログラミングのための機能を追加することから始まった言語であるC++Objective-Cと比較すれば、そのシンプルさは一目瞭然だろう。

C++の仕様は大きく複雑だ。C++の全ての側面に精通しているプログラマは、決して多くはない*1C言語には、C++で追加された様々な機能が存在しない。つまり、それだけシンプルだ。

Objective-Cは、C言語Smalltalk流のオブジェクト指向プログラミング言語を追加したものだ。実質的に2つの言語を抱えている。C言語は、Objective-Cの半分だ。それだけ言語仕様が小さく済んでいる。

C言語の仕様はシンプルで、さほど大きくない。言語仕様的に微妙なところがあるので、やさしいとは言えない。しかし難しい訳でもない。C言語自体は、微妙な表現だが、難しくはない。

C言語でのプログラミングは難しい:仮説「低水準だから?」

しかしながら、いざC言語による現実のプログラミングを学ぼうとすると、これがなかなか難しい。C言語で実用的な何かを書いてみると、その難しさは疑惑から確信に変化する。

C言語自体は難しくないのに、C言語で何かを成すのは難しい。なぜだろうか?

C言語は高水準言語の中では低水準の部類に属する。この辺が原因だろうか? ――影響はあるが、主要な原因ではないだろう。

C言語の言語本体や標準ライブラリで提供される機能は少ないが、しかし世の中にはC言語で利用できるライブラリが山ほどある。C言語ガベージコレクションはないが、例えばBoehm GCがある。C言語にはオブジェクト指向プログラミングのための機能はないが、例えばGLibのGObjectがある。C言語には正規表現の機能はないが、例えばPOSIXregexがある。

最近のプログラミング言語のようなパッケージ管理システム*2は存在しないが、しかし例えばUnixユーザランド環境はCプログラミングにおいてある種のプラットホームとして機能する。ディストリビューションのパッケージ管理システムを使用して手軽にライブラリを導入できるだけでなく、TCP/IP通信やカーネルの管理機能をC言語で利用することもできる。

Common LispSchemeのマクロにははるかに及ばないが、しかしC言語の枠内にも合法的に機能を拡張する仕組みが存在する*3。それらの機能を使用して、C言語にさらなる抽象化層を提供するライブラリその他が揃っている。そういったライブラリ等を用いることで、Cプログラミングにおける抽象化の水準を高くすることが可能だ。

抽象化層を提供するライブラリを用いることで、C言語でのプログラミングはある程度は容易になる。抽象化の度合いが変化することで、プログラミングの難易度も変化する。

ただし、C言語の低水準な部分がCプログラミングの難しさの主な原因である、とまでは言えないだろう。もし低水準であることが原因ならば、便利なライブラリを積極的に使用して抽象度を上げることで、Cプログラミングの難易度は低下するはずだ。しかし実際には、抽象度を上げることで問題が解決される範囲は限られている。

これは、どういうことだろうか?

C言語」という抽象化層にあく穴

C言語で何かを成すのは難しい一番の理由は、C言語が提供する抽象化層が低水準だからではなく、その抽象化層が脆く容易に穴が開いてしまう代物だという点にある。

例えばC言語学習において悪名高いポインタだが、C言語C++以外にも存在する。有名な言語としてはC#・Go・Swiftがある。

このポインタという機能、C#・Go・Swiftにおいては、C言語C++のそれよりも安全になっている。しかし実際に使用すると、それぞれの言語の抽象化層に穴があいてしまうことがある。

C#での例を示そう。C#にはガベージコレクションがあり、普段はオブジェクトが使用するメモリの確保や解放のことなど全く気にする必要がないし、オブジェクトのアドレスとは無縁で済む。しかしポインタを使用すると、抽象化層に隠されたメモリ管理の部分とつきあわなくてはならなくなる。C#の変数が固定変数と移動可能変数に分類可能であることや、その2種類の変数の振る舞いの差異は、普段は抽象化層に隠されている。しかしポインタを使用すると抽象化層に穴があき、白日の下に晒されてしまう。だからfixedステートメントが存在するのだ。

C言語C++においても、ポインタは抽象化層に穴をあける代物だ。しかも悪いことに、C#・Go・Swiftのポインタよりも強力で野蛮だ。抽象化層にあく穴は、より大きく深いものとなる。

C言語C++にはポインタ以外にも、例えば配列の二級市民ぶりだとか、文字列についてのあれこれだとか、動的メモリ管理まわりだとか、抽象化層に穴があく契機となるものが山のように存在する。C++においては、参照や標準ライブラリの機能を積極的に使用することで、これらの側面を回避できる余地がある。しかしC言語でのプログラミングにおいては、いずれの機能も使わずに済まそうとしたら何もできなくなる。つまり、危険を承知で使わざるをえない。

ここにC言語でプログラミングする時の難しさがある。抽象化層の特定の層にて安穏としていることができず、頻繁に下位層の事情を気にしなくてはならない。プログラミングという高度な知的活動においては、非本質的(偶有的)な部分の障害を減らすことが鉄則だが、C言語ではそれがままならない。

C言語という抽象化層が、たとえ低水準であっても、丈夫で穴が開きにくい代物だったならば、事情は違っていただろう。しかしその代償として、カーネルデバイスドライバファームウェアなどの低水準の領域でC言語が使われることもなかったに違いない。抽象化層に自由に穴を開けて下位層にアクセスできるからこそ、低水準の領域でのプログラミングが可能なのだから。

学習コスト

Cプログラミングにて絶えず下位層の事情を気にしなくてはならないという点は、Cプログラミングの学習コスト増加につながっている。すなわち、C言語の言語機能やライブラリ以外にも山ほど学ぶことがあり、それらを学んでおかないと思わぬところで自分の足を撃ち抜いてしまう、ということだ。

「良いプログラミング・スタイル」という観点では、どのような言語を用いても「言語機能やライブラリ以外にも学ぶべきこと」が出てくるものだが、C言語が特異なのは「コンピュータの振る舞い」を学ばないとまともなコードを書けないことだ。特にメモリの使われ方を(学習用に抽象化された内容でもよいから)押さえておかないと、C言語の変数とポインタにまつわるセマンティクス絡みでバグを仕込んでしまう確率が高くなる。

間が悪いことに、C言語の初学者からすれば、どこまでがC言語の範疇でどこからが下位層の話になるのか、境界が明確ではない。この問題は学習者を惑わせるだけでなく、C言語を教える立場の人の悩みのタネでもある。

まとめ

C言語自体はシンプルでさほど難しくないが、C言語でのプログラミングを学んだり、C言語で実用的なソフトウェアを書いたりすることは難しい。

この難しさは、C言語という抽象化層が薄く容易に穴が開きやすいことから生じている。この「抽象化層の薄さ」はC言語のメリットであり、デメリットである。低水準の領域にてC言語でプログラムを記述できる点が、「抽象化層の薄さ」のメリットである。C言語で何かをなすのが難しい点は、「抽象化層の薄さ」のデメリットである。

*1:かといって無茶苦茶少ない訳でもないが。

*2:例えばRubyのgemやNode.jsのnpmのような機能のこと。

*3:例えば関数、構造体、共用体、不完全型、列挙体、typedef、プリプロセスによるマクロ置換など。