ブログ 約6分

Linus Torvalds のC++への意見を理解してみる

Share this article
Linus Torvalds のC++への意見を理解してみる

Linux、PHP、GitはCで開発された人気のプロジェクトです。一方、OpenOffice、Firefox、Clang、PhotoshopはC++で開発されており、複雑なアプリケーションの開発にはどちらの言語も適していることが分かります。一方が他方より優れていることを証明しようとするのは、あまり有益な議論ではないかもしれません。しかし、一方を他方より選ぶ背景にある動機については議論できます。

Linus TorvaldsのC++に関する 意見 を初めて読んだとき、C++開発者として、私は彼の見解にまったく同意できませんでした。しかし、これはLinuxカーネルとGitのリード開発者の視点であり、慎重に検討する価値があります。

彼の意見を読み返した後、私は次の主張に同意していることに気づきました:

inefficient abstracted programming models where two years down the road you notice that some abstraction wasn't very efficient, but now all your code depends on all the nice object models around it, and you cannot fix it without rewriting your app.

確かにC++は、エレガントで適切に構造化されたコードを書くための多くの可能性を提供します。しかし、それには代償があります。変更やリファクタリングが困難になる可能性があるのです。とはいえ、だからといって別の言語を選ばなければならないわけではありません。すべての言語やライブラリにはトレードオフがあり、長年の開発の後でC++コードに変更が必要になった場合に、その潜在的な影響をどう限定するかを知っておく必要があります。

ここでは、 Git のソースコードを CppDependで分析し、いくつかの設計上の特徴を特定し、次の2つの領域でCとC++を比較してみましょう:

  • 理解しやすさ。
  • 変更の管理。

モジュール性:物理的 vs 論理的

モジュール性は、ソフトウェアが独立した部分で構成される度合いを高めるソフトウェア設計技法です。モジュール化されたコードは、管理・保守が容易です。

プロジェクトをモジュール化するには、2つのアプローチがあります:

  • 物理的:ディレクトリとファイルを使用する。この形のモジュール性はオペレーティングシステムによって提供され、あらゆる言語に適用できます。
  • 論理的:名前空間、コンポーネント、クラス、構造体を使用する。この技法は言語の機能に依存します。

Cで開発する場合、主に物理的なモジュール性を使用してコードを整理します。ディレクトリはモジュールを分離するために使用されます。Gitの一部のディレクトリ間の依存関係グラフは次のとおりです:

一方C++では、名前空間を使用してコードベースをモジュール化できます。これらの仕組みは言語自体によって提供されます。前のグラフでは、図形はディレクトリではなく、コードをモジュール化する名前空間であると考えることもできます。

これら2つのアプローチのどちらかを選択することの影響:理解しやすさ:論理的アプローチの方が優れています。モジュール性が言語構造によって明確に定義されるため、コードを読むだけで、コード要素がどのモジュールに属するかを判断できるからです。

変更の管理:優れた設計には一般に多くの反復が必要です。物理的アプローチでは、設計変更の影響を論理的アプローチよりもはるかに限定できます。関数や変数を別のファイルへ移動したり、ファイルを別のディレクトリへ移動したりするだけで済む場合があります。

一方C++では、そのような変更が多くのコードに影響する可能性があります。論理的なモジュール性は言語構造を通じて実装されるため、コード変更が必要になるからです。

カプセル化:クラス vs ファイル

C++では、カプセル化はデータと関数をクラスと呼ばれる単一の単位にまとめるプロセスとして定義されます。カプセル化により、プログラマーはデータへ直接アクセスできなくなり、データはクラス内にある関数を通じてのみアクセスできます。

Cでもカプセル化を実現できますが、モジュール性のセクションで説明したような物理的アプローチを使用します。ファイルに関数とそれらが使用するデータを含め、関数と変数の可視性を「static」キーワードで制限できます。

Gitは、この技法を使って関数と変数を隠蔽しています。それを確認するために、静的関数を検索してみましょう:

from m in Methods where m.IsStatic select m

ツリーマップは、CQLinqクエリに一致したコード要素の概要を把握するうえで非常に有用です。青い長方形が結果を表しています。

ほぼすべての関数がstaticとして宣言され、宣言された翻訳単位内でのみ可視になっています。変数についても同様です。

from f in Fields where f.IsStatic select f
理解しやすさ:

C++のカプセル化機構を使用すると、コードの可読性と整理が向上します。Cはより低レベルであり、論理的アプローチよりも物理的アプローチに依存します。

変更の管理:変数や関数をカプセル化する場所を変更する必要がある場合、Cでは非常に簡単なことがあります。一方C++では、多くのコードに影響する可能性があります。

ポリモーフィズム vs 選択イディオム

ポリモーフィズムとは、コード、操作、オブジェクトが異なるコンテキストで異なる振る舞いをすることを意味します。

この技法はC++プロジェクトで広く使用されていますが、Cではどうでしょうか?

手続き型言語では、「switch」「if」、さらには「goto」などのキーワードを使って選択が実装されます。しかし、この技法はコードの循環的複雑度を高める傾向があります。

Gitのソースコード内で複雑な関数を検索してみましょう。

Gitは適切に設計されていますが、複雑と見なせる関数が多数あります。これは部分的に、「if」「switch」「goto」などの制御フロー文が多用されているためです。一方C++では、ポリモーフィズムを使用してコードの複雑さを軽減できます。

理解しやすさ:ポリモーフィズムを使用すると、特定の振る舞いをクラス内に分離できるため、コードの可読性と凝集性が向上します。

変更の管理:ポリモーフィズムで別の振る舞いを追加するには、別のクラスを追加する必要がある場合があります。一方、選択イディオムでは、switch文に別のcaseを追加するだけで済みます。

継承 vs コンポジション

Gitは主に、関数が操作するデータを定義するために構造体を使用しています。Gitが使用しているすべての構造体を検索してみましょう:

from t in Types where t.IsStructure select t

興味深いことに、ほぼすべてのデータが構造体内に含まれています。これを確認するために、構造体内にない、非constの公開プリミティブ変数をすべて検索できます:

from f in Fields where f.IsPublic && f.IsPrimitiveType
&& !f.IsStatic && !f.IsConst
select f

このクエリに一致する変数はごく少数であり、これはGitの設計における良い点です。

では、構造体を拡張する場合はどうでしょうか。Cでは、多くの構造体が参照する「remote」構造体のように、コンポジションを使用できます。

一方C++では、継承を使って構造体を拡張することもできます。たとえば、known_remote構造体はremoteを継承できます。

理解しやすさ:継承を使用するとデータ間の関係を理解しやすくできますが、慎重に使用する必要があります。「is-a」関係の場合にのみ使用すべきです。

変更の管理:継承はより強い結合を導入するため、1つの変更が多くのコードに影響する可能性があります。

結論:

C++は、エレガントで適切に構造化されたコードを書くための多くの可能性を提供します。しかし、それには代償があります。変更やリファクタリングが困難になる可能性があるのです。

リファクタリングでは、変更を加える前に既存コードを理解する必要があります。Cプログラムは理解が難しい一方で変更は容易な場合があります。C++プロジェクトはCプロジェクトよりも構造化されている場合がありますが、変更時にはより多くの労力を要する可能性があります。

C++で変更の影響を限定するにはどうすればよいでしょうか?

OOPアプローチを使用する場合に変更の影響を限定する良い方法は、デザインパターン、特に疎結合と高凝集の原則を適用して、変更を特定の領域に分離することです。

もう1つの効果的なアプローチは、ジェネリックプログラミングと現代的なC++の実践を採用することです。ジェネリックプログラミングはOOPよりも柔軟であり、C++コードへの変更の影響を限定するのに役立ちます。

Share this article