大規模なソフトウェア・プロジェクトは通常、それに応じた大規模な開発者チームによって行われます。大規模なチームによって作成されたコードがプロジェクト全体で測定可能な品質を持つには、 標準に従い、標準に照らして評価されるコードを作成する必要があります。したがって、大規模なプロジェクト・チームにとっては、 プログラミングの標準、つまり一連のガイドラインを確立することが重要です。
プログラミング標準を使用することによって、以下のことが可能になります。
このマニュアルの目的は、標準として使用できる C++ のプログラミング規則、ガイドライン、 ヒント (一般にガイドラインと呼ばれる) を提示することです。このマニュアルは、 大規模なプロジェクト・チームに参加しているソフトウェア・エンジニアを対象としています。
現状版では、プログラミング (プログラミングと設計を明確に区別することは難しいこともあるが) に焦点を当てます。 設計ガイドラインは後日追加します。
このマニュアルでは、C++ 開発における以下のガイドラインを扱います。
これらのガイドラインは、広範な業界知識ベースから収集したものです (原典、著者、参考情報について詳しくは、 「参考文献」を参照してください)。以下のカテゴリーに基づいています。
ほとんどのガイドラインは、2 番目と 3 番目のカテゴリーに基づいており、1 番目のカテゴリーも多少関係しています。 一部のガイドラインは最後のカテゴリーに基づいています。 これは、プログラミングはきわめて主観的な作業であり、 すべてをコーディングするための、広く受け入れられた「最善の」または「正しい」方法など存在しないからです。
明瞭で理解しやすい C++ ソース・コードを作成することが、多くの規則とガイドラインの主な目標です。 ソース・コードが明瞭で理解しやすいと、ソフトウェアの信頼性と保守容易性が大きく向上します。明瞭で理解しやすいコードが意味することは、 以下の 3 つの単純な基本原則として把握できます (参考資料 [Kruchten, 94])。
最小限の意外性 - ソース・コードはその存続期間に渡って、作成されるよりも 読まれる回数のほうが多く、特に仕様についてそれが当てはまります。 コードは、実行される内容と実行による利益を英語の説明を読むように理解できるのが理想的です。プログラムは、コンピューターに向けてというよりは、 人間のために作成されます。コードを読むことは複雑な知的プロセスであり、その労力は統一性によって軽減されます。 このガイドでは、そのような統一性を最小限の意外性原則と呼んでいます。プロジェクト全体で統一されたスタイルは、 ソフトウェア開発者のチームがプログラミング標準に関して同意するための重要な根拠です。 ある種の罰則または創造性や生産性を妨げる障害として受け止めてはいけません。
単一の保守ポイント - 設計上の決定は、できる限りソース内のただ 1 つの場所だけに 記述し、その決定による帰結のほとんどは、そのポイントからプログラマチックに 派生しなければなりません。この原則に違反すると、 保守容易性と信頼性に加えて、理解のしやすさも大きく損なわれます。
最小限の雑音 - 最後に、コードを読みやすくするための重要な要素として、 最小限の雑音原則を適用します。つまり、 ソース・コードが視覚的な「雑音」によって雑然となることを回避するために労力をかけます。 雑音とは、各種のバー、ボックス、重要度の低い情報またはソフトウェアの目的の理解には寄与しない情報を含むその他のテキストなどです。
ここに示すガイドラインの意図は、過度に制約を課すことではなく、言語の機能を正しく安全に使用するためのガイダンスを提供することです。優れたソフトウェアを作成するための鍵は、次のとおりです。
本書に記載するガイドラインには、次の前提事項があります。
ガイドラインはすべてが同じ重要度を持っているわけではありません。以下の基準によって分類されます。
この記号で示されるガイドラインは、参考にできる簡単なアドバイスです。従わなかったとしても問題はありません。
この記号で示されるガイドラインは、より技術的な根拠に基づいていることが多い推奨事項です。 カプセル化、凝集性、結合性、移植性、再利用性に影響する場合があり、実装によっては性能にも影響します。 これらの推奨事項は、使用しない正当な理由がないかぎり、従う必要があります。
この記号で示されるガイドラインは、要求または制限です。これらに違反すると、必ず不明瞭で、信頼性が低く、移植性のないコードになります。 要件または制限に違反することは、許容されません。
適切な規則またはガイドラインが見つからない場合、規則が明らかに適用されていない場合、 何もかもが失敗する場合は、常識的な考え方に基づいて、基本的な原則を確認します。この規則は、 他のどの規則よりも優先されます。規則やガイドラインが存在する場合でも常識は必要です。
この章では、プログラムの構造とレイアウトに関するガイダンスを示します。
大規模システムは通常、数多くの小規模な機能上のサブシステム として 開発されます。 サブシステム自体は一般的に数多くのコード・モジュール から 構築されます。 C++ のモジュールには、通常単一の、場合によっては緊密に関連した 一連の抽象化 実装が含まれます。C++ では、抽象化はクラス として 実装されるのが一般的です。クラスは 2 つの異なるコンポーネントを持ちます。1 つは クラスのクライアントから見ることのできるインターフェース で、クラスの機能と 責務の宣言または仕様を提供します。もう 1 つは、宣言された仕様の実装 (クラス定義) です。
クラスと同様に、モジュールにもインターフェースと実装があります。 モジュールのインターフェースには、包含されるモジュールの抽象化の仕様 (クラス宣言) が 含まれ、モジュールの実装には、抽象化の実際の実装 (クラス定義) が含まれます。
システムの構築では、サブシステムを共同グループ内またはレイヤー内に構成して、 その依存性を最小化し、制御する場合もあります。
モジュールの仕様は実装とは別のファイルに配置する必要があります。 仕様ファイルをヘッダー と呼びます。モジュールの実装は 1 つ以上の 実装ファイルに配置できます。
モジュールの実装に数多くのインライン関数、共通の implementation-private 宣言、 テスト・コード、プラットフォーム固有のコードが含まれる場合は、これらの部品を それぞれのファイルに分離し、そのコンテンツに基づいて各部品ファイルに名前を付けます。
実行可能プログラムのサイズが重要な場合は、ほとんど使用されない関数も 個別のファイルに配置する必要があります。
部品ファイルには次の方法で名前を付けます。
File_Name::=
<Module_Name> [<Separator> <Part_Name>] '.' <File_Extension>
モジュールの分割、命名方式の例を以下に示します。
inlines
ファイルに配置します (「モジュールのインライン関数定義を別々のファイルに配置します」を参照)。Module.Test
と
命名する必要があります。テスト・コードを friend クラスとして宣言することによって、
モジュールとそのテスト・コードの独立開発が容易になり、ソースを変更することなく、
最終的なモジュール・オブジェクト・コードからテスト・コードを除外できます。SymaNetwork.hh // Contains the declaration for a // class named "SymaNetwork". SymaNetwork.Inlines.cc // Inline definitions sub-unit SymaNetwork.cc // Module's main implementation unit SymaNetwork.Private.cc // Private implementation sub-unit SymaNetwork.Test.cc // Test code sub-unit
モジュールの仕様をその実装から分離することによって、 ユーザー・コードとサプライヤー・コードの独立開発が容易になります。
モジュールの実装を複数の翻訳単位に分割すると、オブジェクト・コードの削除をサポートしやすくなり、 結果的に実行可能ファイルのサイズが小さくなります。
通常の、予測可能なファイル命名規則と分割規則を使用することによって、 モジュールのコンテンツと構造を実際に調べることなく理解できます。
コードで使用される名前をファイル名でも使用することによって、予測可能性が向上し、 複雑な名前マッピングを必要とせずにファイル・ベースのツール構築が容易になります (参考資料 [Ellemtel, 1993])。
一般的に使用されるファイル名拡張子として、ヘッダー・ファイル用に .h、.H、.hh、.hpp、
.hxx
、
実装ファイル用に .c、.C、.cc、.cpp、
.cxx
があります。拡張子のセットを選択し、それを一貫して使用します。
SymaNetwork.hh // The extension ".hh" used to designate // a "SymaNetwork" module header. SymaNetwork.cc // The extension ".cc" used to designate // a "SymaNetwork" module implementation.
C++ の標準化文書 (草案) では、名前空間によってカプセル化されたヘッダーに、拡張子「.ns」も使用しています。
ごくまれに複数のクラスを 1 つのモジュール内に配置する必要のある場合がありますが、 そのような処理が実際に行われるのは、クラスが密接に関連している場合だけです (コンテナーとその反復子など)。すべてのクラスがクライアント・モジュールから常に見える必要がある場合は、 モジュールのメイン・クラスとそのサポート・クラスを同じヘッダー・ファイルに配置できます。
モジュールのインターフェースと、それに対するほかからの依存性を削減します。
クラスの private メンバーを除き、 モジュールの implementation-private 宣言 (実装の種類やサポート・クラスなど) をモジュールの仕様に記述しないようにします。これらの宣言は、 必要とされる実装ファイル内に配置する必要があります。ただし、宣言が複数の実装ファイルによって必要とされる場合を除きます。 その場合、宣言はセカンダリー の Private ヘッダー・ファイルに配置する必要があります。セカンダリーの Private ヘッダー・ファイルは、 必要に応じて、その他の実装ファイルによってインクルードされる必要があります。
この実践原則によって、次のようなことが確実になります。
// Specification of module foo, contained in file "foo.hh" // class foo { .. declarations }; // End of "foo.hh" // Private declarations for module foo, contained in file // "foo.private.hh" and used by all foo implementation files. ... private declarations // End of "foo.private.hh" // Module foo implementation, contained in multiple files // "foo.x.cc" and "foo.y.cc" // File "foo.x.cc" // #include "foo.hh" // Include module's own header #include "foo.private.hh" // Include implementation // required declarations. ... definitions // End of "foo.x.cc" // File "foo.y.cc" // #include "foo.hh" #include "foo.private.hh" ... definitions // End of "foo.y.cc"
#include
を使用してモジュールの仕様にアクセスします別のモジュールを使用するモジュールは、
プリプロセッサーの #include
指令を使用して、サプライヤー・モジュールの仕様の可視性を得る必要があります。
それに付随して、モジュールはサプライヤー・モジュールの仕様のいかなる部分も再宣言してはいけません。
ファイルをインクルードするときは、「標準」ヘッダーについては #include <header>
構文だけを使用し、
それ以外については #include
「ヘッダー」構文を使用します。
#include
指令の使用は、モジュール自身の実装ファイルにも適用されます。モジュールの実装には、
それ自身の仕様ヘッダーと Private セカンダリー・ヘッダーをインクルードする必要があります (「モジュールの仕様と実装を個別のファイルに配置します」を参照)。
// The specification of module foo in its header file // "foo.hh" // class foo { ... declarations }; // End of "foo.hh" // The implementation of module foo in file "foo.cc" // #include "foo.hh" // The implementation includes its own // specification ... definitions for members of foo // End of "foo.cc"
#include
規則の例外は、
モジュールがサプライヤー・モジュールの型 (クラス) を by-reference (ポインターまたは reference-type 宣言を使用) によって使用または保有するのみである場合です。
この場合、by-reference の使用または保有は、#include
指令ではなく、forward 宣言を使用して指定します (「コンパイルへの依存性を最小化します」も参照)。
必要以上のヘッダーをインクルードしないでください。 これは、モジュール・ヘッダーには、モジュール実装によってのみ必要とされるその他のヘッダーをインクルードしてはいけないことを意味します。
#include "a_supplier.hh" class needed_only_by_reference;// Use a forward declaration // for a class if we only need // a pointer or a reference // access to it. void operation_requiring_object(a_supplier required_supplier, ...); // // Operation requiring an actual supplier object; thus the // supplier specification has to be #included. void some_operation(needed_only_by_reference& a_reference, ...); // // Some operation needing only a reference to an object; thus // should use a forward declaration for the supplier.
この規則によって、以下のことが保証されます。
モジュールに数多くのインライン関数が含まれる場合、それらの定義は個別のインライン関数専用のファイルに配置する必要があります。インライン関数ファイルは、 モジュールのヘッダー・ファイルの最後に包含する必要があります。
「No_Inline
条件付きコンパイル・シンボルを使用して、インライン・コンパイルを排除します」も参照してください。
この手法を使用すると、モジュールのヘッダーが実装の詳細によって雑然となることを防ぐことができます。 そのため、明瞭な仕様を維持できます。また、インラインでコンパイルしないときにはコードの複製を削減するのにも役立ちます。 条件付きコンパイルを使用することによって、インライン関数を、 使用中のすべてのモジュールに静的にコンパイルするのではなく、1 つのオブジェクト・ファイルにコンパイルできます。それに付随して、 インライン関数の定義は、本当にわずかな量でないかぎりは、クラス定義内に包含しないでください。
大規模なモジュールを複数の翻訳単位に分割して、 プログラム・リンク時の非参照コードの削除を容易にします。ほとんど参照されることのないメンバー関数は、 頻繁に使用されるファイルとは別のファイルに分離する必要があります。極端に言えば、 個々のメンバー関数は各自のファイルに配置できます (参考資料 [Ellemtel, 1993])。
すべてのリンカーが等しくオブジェクト・ファイル内の非参照コードを除外できるわけではありません。大規模なモジュールを複数のファイルに分割すると、 リンカーは、オブジェクト・ファイル全体のリンクを回避することによって、 実行可能プログラムのサイズを削減できます (参考資料 [Ellemtel, 1993])。
最初に、モジュールをより小さな抽象化に分割する必要があるかどうかを考慮することも価値がある場合があります。
プラットフォーム依存のコードをプラットフォーム非依存のコードから分離します。 これによって、移植が容易になります。プラットフォーム依存のモジュールには、プラットフォーム名によって修飾されるファイル名を付けることによって、 プラットフォームへの依存を強調する必要があります。
SymaLowLevelStuff.hh // "LowLevelStuff" // specification SymaLowLevelStuff.SunOS54.cc // SunOS 5.4 implementation SymaLowLevelStuff.HPUX.cc // HP-UX implementation SymaLowLevelStuff.AIX.cc // AIX implementation
アーキテクチャーや保守の観点からいうと、 プラットフォーム依存を少数の低レベルのサブシステムに含めることもよい実践原則です。
標準的なファイル・コンテンツ構造を採用し、それを一貫して適用します。
推奨されるファイル・コンテンツ構造は、以下の部分によって以下の順序で構成されます。
1. 重複インクルードの保護 (仕様のみ)。
2. ファイルとバージョンの管理 ID (オプション)。
3. このユニットに必要なファイルのインクルード。
4. モジュール文書 (仕様のみ)。
5. 宣言 (クラス、型、定数、オブジェクト、関数) とその他のテキスト仕様 (事前条件と事後条件、不変)。
6. モジュールのインライン関数定義のインクルード。
7. 定義 (オブジェクトと関数) と implementation private 宣言。
8. 著作権情報。
9. バージョン管理履歴。
以上のファイル・コンテンツ順序では、クライアントに関連する情報が最初に示され、クラスの public、protected、private の各セクションが順序付けされる原理と一致しています。
会社のポリシーによっては、著作権情報をファイルの先頭に配置しなければならない場合があります。
ファイルの重複インクルードと重複コンパイルは、各ヘッダー・ファイルで次の構成要素を使用して防ぐ必要があります。
#if !defined(module_name) // Use preprocessor symbols to #define module_name // protect against repeated // inclusions... // Declarations go here #include "module_name.inlines.cc" // Optional inline // inclusion goes here. // No more declarations after inclusion of module's // inline functions. #endif // End of module_name.hh
インクルード保護シンボルにはモジュール・ファイル名を使用します。シンボルでもモジュール名と同様に大文字と小文字を使い分けます。
No_Inline
」条件付きコンパイル・シンボルを使用して、インライン・コンパイルを無効にします
次の条件付きコンパイル構成要素を使用して、インライン可能な関数のインライン・コンパイルとアウトオブライン・コンパイルを制御します。
// At the top of module_name.inlines.hh #if !defined(module_name_inlines) #define module_name_inlines #if defined(No_Inline) #define inline // Nullify inline keyword #endif ... // Inline definitions go here #endif // End of module_name.inlines.hh // At the end of module_name.hh // #if !defined(No_Inline) #include "module_name.inlines.hh" #endif // At the top of module_name.cc after inclusion of // module_name.hh // #if defined(No_Inline) #include "module_name.inlines.hh" #endif
条件付きコンパイルの構成要素は、複数のインクルード保護の構成要素に似ています。No_Inline
シンボルが定義されていない場合、
インライン関数はモジュール仕様と共にコンパイルされ、モジュール実装から自動的に除外されます。No_Inline
シンボルが定義されている場合、
インライン定義はモジュール仕様から除外されますが、モジュール実装にはキーワード inline
が無効になった状態でインクルードされます。
以上の手法によって、インライン関数がアウトオブラインでコンパイルされたときのコードの複製を削減できます。条件付きコンパイルを使用することによって、 インライン関数の単一コピーが、定義するモジュールにコンパイルされます。 それに対し、複製コードは、コンパイラー・スイッチによってアウトオブライン・コンパイルが指定されていると、「static」 (内部リンケージ) 関数として、 すべての使用中のモジュールでコンパイルされます。
条件付きコンパイルの使用によって、ビルド依存性の保守が複雑になります。この複雑性は、 ヘッダーとインライン関数定義を常に 1 つの論理ユニットとして扱うことによって管理されます。 したがって、実装ファイルはヘッダー・ファイルとインライン関数定義ファイルの両方に依存します。
一貫性のあるインデントを使用して、ステートメントがネストされていることを視覚的に表現する必要があります。 この目的のためには、スペースを 2 ~ 4 使用することが視覚的に最も効果的であることが実証されています。通常のスペース 2 つによる インデントを使用することをお勧めします。
複合ステートメントまたはブロック・ステートメントの区切り文字 ({}
) は、
前後の記述とインデントのレベルが同じである必要があります (つまり、{}
が縦方向に整列します)。ブロック内のステートメントは、
スペースの数を決めてインデントする必要があります。
switch
ステートメントの case ラベルは、switch
ステートメントと
インデントのレベルが同じである必要があります。switch
ステートメント内の
ステートメントは、switch
ステートメントと case ラベルから 1 レベル下げてインデントできます。
if (true) { // New block foo(); // Statement(s) within block // indented by 2 spaces. } else { bar(); } while (expression) { statement(); } switch (i) { case 1: do_something();// Statements indented by // 1 indentation level from break; // the switch statement itself. case 2: //... default: //... }
2 つのスペースによるインデントは、ブロックを簡単に認識できるようにするためと、コードがモニターや印刷ページの右側からはみ出さない程度にネスト・ブロックの空間を確保するための折衷案です。
関数の宣言が 1 行に収まらない場合は、先頭のパラメーターを関数名と同じ行に配置します。 以降のパラメーターはそれぞれ新しい行に配置し、先頭のパラメーターと同じレベルにインデントします。次に示すとおり、 この宣言とインデントのスタイルでは、関数の戻り値の型と名前の下に空白ができるため、可視性が向上します。
void foo::function_decl( some_type first_parameter, some_other_type second_parameter, status_type and_subsequent);
このガイドラインに従った結果、行が折り返したり、パラメーターのインデント余白が大きくなる場合は、関数名またはスコープ名 (クラス、名前空間) からすべてのパラメーターをインデントし、それぞれを個別の行に配置します。
void foo::function_with_a_long_name( // function name is much less visible some_type first_parameter, some_other_type second_parameter, status_type and_subsequent);
次の位置合わせ規則も参照してください。
プログラム行の最大長は、標準 (レター) またはデフォルトの印刷用紙サイズに印刷したときに、情報が切り捨てられないように制限する必要があります。
インデントのレベルによってネストの深いステートメントが右に寄り、右の余白を大幅に超える場合は、より小さく、管理しやすい関数にコードを分割することを考慮することをお勧めします。
関数の宣言、定義、呼び出し内でのパラメーター・リストや、enum 宣言内の列挙子が 1 行に収まらない場合は、各リスト要素の後で改行し、 各要素を個別の行に配置します (「関数のパラメーターを関数名またはスコープ名からインデントします」も参照)。
enum color { red, orange, yellow, green, //... violet };
クラスまたは関数のテンプレート宣言が長くなりすぎる場合は、 テンプレートの引数リストの後で改行します。次に例を示します (標準の反復子ライブラリーからの宣言、参考資料 [X3J16, 95])。
template <class InputIterator, class Distance> void advance(InputIterator& i, Distance n);
この章では、コード内でのコメントの使用に関するガイダンスを示します。
コメントはソース・コードを補足する ためのものであり、 言い換えるためのものではありません。
各コメントについて、 プログラマーは「このコメントによりどのような価値を加えられるのか。」という問いに十分な回答を用意できなければなりません。 一般的に、適切な名前を選択することによってコメントの必要がなくなることが多くあります。コメントは、一部の形式的なプログラム設計言語 (PDL) で使用する場合を除き、 コンパイラーによってチェックされることはありません。したがって、保守の単一点原則に基づき、設計上の決定はコメント内ではなく、 ソース・コード内で表現する必要があります。宣言が多少増えても、この原則に従います。
C 形式のコメント区切り文字「/*...*/
」ではなく、C++ 形式のコメント区切り文字「//
」を使用してください。
C++ 形式のコメントは、C 形式より見やすく、コメント終了の区切り文字を 入れ忘れることによって大量のコードをコメント化してしまうリスクが軽減されます。
/* start of comment with missing end-of-comment delimiter do_something(); do_something_else(); /* Comment about do_something_else */ // End of comment is here ---> */ // Both do_something and // do_something_else // are accidentally commented out! Do_further();
コメントは、その対象となるコードの近くに配置する必要があります。 コードと同じレベルのインデントによって、空のコメント行を使用してコードにつなげます。
複数の連続するソース・ステートメントに適用するコメントは、ステートメントの上 に配置して、 ステートメントの導入部として機能させます。同様に、個々のステートメントに関連するコメントは、 ステートメントの下 に配置します。
// A pre-statements comment applicable // to a number of following statements // ... void function(); // // A post-statement comment for // the preceding statement.
コメントはソース構成要素と同じ行で使用しないでください。多くの場合、配列が崩れます。ただし、enum 宣言での列挙子など、 長い宣言の要素については、そのようなコメントを使用してもかまいません。
作成者、電話番号、作成日、修正日などの情報を含むヘッダーは使用しないでください。 作成者と電話番号は変わりやすいためです。 作成日、修正日、修正の理由は、構成管理ツール (またはその他の バージョン・ヒストリー・ファイル) によって保守することをお勧めします。
垂直バーや囲みは、主要な構成要素 (関数やクラスなど) であっても使用しないようにします。これらは視覚的に邪魔になるだけでなく、 一貫性を保ちにくくします。関連のあるソース・コードのブロックを分割するときは、ブランク行を使用し、 以下のような重いコメント行は避けます。関数またはクラス内の構成要素を区切るときは、 ブランク行を 1 行使用します。関数同士を区切るときは、ブランク行を 2 行使用します。
フレームやフォームは見た目に統一性があり、コードの記述が必要であることをプログラマーに示す役割がありますが、 言い換えの形式になってしまうことが多くあります (参考資料 [Kruchten, 94])。
1 つのコメント・ブロック内で段落を区切るには、空行ではなく、空のコメントを使用します。
// Some explanation here needs to be continued // in a subsequent paragraph. // // The empty comment line above makes it // clear that this is another // paragraph of the same comment block.
コメント内でプログラム ID を繰り返したり、ほかの場所にある情報を重複して記述したりしないでください。 後の場合は、情報へのポインターを提供します。そうしなかった場合、プログラムを変更する際に、 複数の場所で保守が必要になります。必要なコメント変更をすべての場所で行わなかった場合、誤解を生じるような、間違ったコメントになり、 コメントがないときよりも悪い状態になってしまいます。
コメントを提供するというよりは、常にコードだけを読んでも意味が分かるようなコードを記述するように心がけます。 これは、適切な名前を選択したり、追加の一時変数を使用したり、 コードを再構成したりすることによって実現できます。コメント内のスタイル、構文、 語のつづりに注意します。電報のような形式や暗号形式ではなく、自然言語のコメントを使用します。
do { ... } while (string_utility.locate(ch, str) != 0); // Exit search loop when found it.これを次のように置き換えます。
do { ... found_it = (string_utility.locate(ch, str) == 0); } while (!found_it);
コードだけでも意味が分かるコードはコメントよりも望ましい形式ですが、 通常は単にコードの複雑な部分を説明する以上の情報を提供する必要があります。必要な情報とは、少なくとも以下の情報を文書化したものです。
宣言に関連したコードの文書化で、クライアントはコードを十分使用できます。文書化が必要な理由は、
クラス、関数、型、オブジェクトの完全なセマンティクスを C++ だけでは十分に表現できないためです。
この章では、さまざまな C++ エンティティーの名前の選択についてのガイダンスを示します。
プログラム・エンティティー (クラス、関数、型、オブジェクト、リテラル、例外、 名前空間) のよい名前を考えるのは容易なことではありません。中規模から大規模のアプリケーションについては、 名前の競合があることからこれはより深刻な問題であり、 同じではないが類似している複数の概念を指し分ける同義語の不足が難しさに拍車をかけます。
命名規則を使用することで、適切な名前を探すことに要する知的労力を軽減することができます。この利点を別にしても、 命名規則にはコードの一貫性を促進するという利点があります。命名規則を有益なものとするためには、 入力上のスタイル (または名前の記述方法) と名前の構造 (または名前の選択方法) についてのガイダンスを提供することが必要です。
一貫して適用されるかぎり、どの命名規則を使用するかはそれほど重要ではありません。命名の統一性のほうが実際の規則よりも重要です。 統一性は「最小限の意外性」の原則を支援します。
C++ は大文字と小文字が区別される言語であり、C++ コミュニティーではさまざまな命名規則が幅広く使用されているため、 命名に関して完全な一貫性を達成できることはほとんどありません。コードの一貫性を できるだけ高めるために、RUP では、ホスト環境 (例: UNIX または Windows) とプロジェクトで使用する原則集に基づいて、 プロジェクトでの命名規則を選択することを推奨します。
注意深い読者の方は、この文書中の例が現時点ではすべてのガイドラインに準拠していないことに気付かれることと思います。 この理由としては、サンプルが複数の出典から引用されていること、 また、紙面の節約のために、箇所によっては書式ガイドラインを厳密に適用していないことが挙げられます。 矛盾する点はありますが、サンプルにおける違反は無視して、実際の作業では本文で述べる規則に従ってください。
ライブラリー関数では、「_main
」や「_exit
」の
ように、1 文字のアンダースコアーを先頭に付けた名前が頻繁に使用されています。
先頭 2 文字のアンダースコアー (「__」) や、
アンダースコアーに英大文字が続く先頭 2 文字の並びは、コンパイラーの内部的使用のために予約されています。
アンダースコアーの正確な数が分かりにくくなることがよくあるため、名前の中でアンダースコアーを連続で使用しないでください。
大文字と小文字の区別以外に違いのない複数の型名がある場合、それらの違いを覚えていることは難しく、型名の混同が容易に起こります。
アプリケーション領域で一般的に認知されている省略語 (例えば、「高速フーリエ変換」の省略語としての「FFT」) や、 プロジェクトで承認された省略語リストに定義されている省略語は使用してもかまいません。それら以外の省略語を使用すると、似ているが完全に同じではない省略語があちこちに出現し、 後で混乱やエラーの原因になる可能性が非常に高くなります (例えば、track_identification が trid、trck_id、tr_iden、tid、tr_ident のようにさまざまに省略される場合など)。
エンティティーの種類を区別するための接尾辞 (型に対しての type や例外に対しての error など) は通常、 コードを読みやすくする目的にはそれほどの効果はありません。array や struct のような接尾辞も特定の実装を暗示しますが、 実装が変化した場合に「struct」や「array」を別の表現に変更する必要があるため、クライアントのコードに悪影響を及ぼしたり、誤解の原因になったりします。
ただし、以下に示すような特定の状況下では接尾辞が役立つこともあります。
使用状況の観点から名前を選択します。ローカルな (状況に固有の) 意味を補強するために、 名詞と共に形容詞を使用します。名前が型と符合することも確認します。
次のような構造の名前を選択します。
object_name.function_name(...); object_name->function_name(...);
このような名前は読みやすく、一目で意味が分かります。
入力が速くなることは、短縮または省略した名前の使用を正当化する理由としては認められません。1 文字の ID や短い ID を選択することは、 多くの場合よくない選択や怠惰さの表れです。自然対数の底を意味する E や、 円周率を意味する Pi を使用するなどの広く認知された事例は例外です。
コンパイラーや補助ツールによっては、名前の長さが制限される場合があります。 そのため、よく似た複数の長い名前がある場合、名前の末尾近くの何文字かだけが違っている、 ということのないように注意する必要があります。そのような違っている部分が、 コンパイラーやツールの文字数制限によって切り捨てられる場合があるためです。
void set_color(color new_color) { ... the_color = new_color; ... }これよりも次の方が望ましいです。
void set_foreground_color(color fg)および
oid set_foreground_color(color foreground);{ ... the_foreground_color = foreground; ... }
最初の例の名前の付けかたは、後の 2 つよりも優れています。new_color
という名前は修飾的で、
その型に符合しており、関数のセマンティクスを分かりやすくしています。
2 番目のケースでは、fg
が foreground (前景) の略であることに気付く可能性もあるかもしれません。
しかし、読み手の直感や推察に理解を委ねることは、どのような優れたプログラミング・スタイルにあっても望ましくないこととされています。
3 番目のケースでは、パラメーター foreground
が (その宣言から離れた場所で) 使用されるとき、
実際には「前景色」を意味するこのパラメーターの意味を読み手は「前景」と理解してしまいがちです。ただし、
読み手が暗黙のうちに「色」という隠れた意味を想像して補い、
パラメーターの意味を「前景色」と読み替える、ということも実際にはよくあります。
名詞と形容詞で名前を形成し、その名前を確実に型と符合させることは自然言語の原則に従っており、コードの読みやすさとセマンティクスの両方を強化します。
名前の中で英単語を使用する場合、正しいスペルを使用し、プロジェクトで要求される形式に従います。 例えば、イギリス英語とアメリカ英語のどちらかを一貫して使用し、両者を混用しないようにします。このことはコメントにも当てはまります。
論理型オブジェクト、関数、関数の引数に対しては、肯定形式の述部の文節を使用します。
例えば、found_it
や is_available
のような形式を使用し、is_not_available
のような形式は使用しません。
述部を否定形で記述するとき、二重否定になると理解しにくくなります。
システムが複数のサブシステムに分解される場合、システムのグローバル名前空間を分割し、 最小化するための名前空間名としてサブシステム名を使用します。 システムがライブラリーである場合、ライブラリー全体に対して 1 つの、 最も外側の層の名前空間を使用します。
各サブシステムまたはライブラリーの名前空間には意味のある名前を付け、
省略語または頭字語の別名を付加します。省略語または頭字語の別名を選ぶときは、競合の可能性が低いものを選びます。
例えば、ANSI C++ 標準草案ライブラリー (参考資料 [Plauger, 95]) では、iso_standard_library
の
別名として std
を定義しています。
コンパイラーが名前空間の構造に未対応の場合、名前の接頭辞を使用して名前空間を模擬します。例えば、 システム管理サブシステムのインターフェース内のパブリックな名前に syma (System Management の略) という接頭辞を付けることができます。
名前空間を使用して潜在的なグローバル名を包含すると、 コードが独立して (サブプロジェクト・チームまたはベンダー単位で) 開発されるときの名前の競合を防ぐために役立ちます。結果として、 名前空間名だけがグローバルになります。
クラスに名前を付けるときは、クラスの抽象作用を表す、 単数形の一般的な名詞または名詞句を使用します。基本クラスにはより一般的な名前を、 派生クラスにはより特化した名前を使用します。
typedef ... reference; // From the standard library typedef ... pointer; // From the standard library typedef ... iterator; // From the standard library class bank_account {...}; class savings_account : public bank_account {...}; class checking_account : public bank_account {...};
オブジェクトと型の両方について、適切な名前の競合または不足があるときは、オブジェクトに単純な名前を使用し、
型名として mode、kind、code
などの接尾辞を追加します。
オブジェクトの集合を表す抽象概念を表現するときは、複数形を使用します。
typedef some_container<...> yellow_pages;
単なるオブジェクトの集合を超えたセマンティクスを付加する必要がある場合、振る舞いのパターンと名前の接尾辞として、標準ライブラリーに定められた次の形式を使用します。
戻り値を持たない関数 (戻り型が void の関数宣言)、または、ポインターまたは参照パラメーターによって値を返す関数に対しては、 動詞またはアクション句を使用します。
非 void の関数戻り型によって単一の値を返す関数については、名詞、または名詞に相当する語句を使用します。
共通の操作群 (振る舞いのパターン) を持つクラスについては、プロジェクトの選択リストから引用した操作名を使用します。begin、end、insert、erase (標準ライブラリー内のコンテナー操作) などがその例です。
「get」や「set」を名前に使用する (関数に「get」や「set」の接頭辞を付ける) 傾向は、 特にオブジェクトの属性を取得、設定するパブリック操作に対しては避けます。操作の命名は、 クラスの抽象化とサービス・レベルの規定にとどめるべきです。 オブジェクト属性の取得や設定は下位レベルの実装の詳細であり、パブリックにするとカプセル化の作用を弱めます。
論理値 (述部) を返す関数については、 形容詞 (または過去分詞) を使用します。述部に関しては、名詞の前に is や has のような接頭辞を付け、名前を肯定的なアサーションとして読めるようにすると効果的なことがよくあります。これは、 オブジェクト、型名、列挙リテラルに対して既に単純名が使用されているときにも効果的です。時制に関して正確さと一貫性を期します。
void insert(...); void erase(...); Name first_name(); bool has_first_name(); bool is_found(); bool is_available();
否定形の名前を使用すると、二重否定を伴う表現 (例: !is_not_found
) が出現してコードが読みにくくなる可能性があるので、
使用しないでください。
場合によっては、反義語を使用することで、否定的な述部をそのセマンティクスを変えることなく肯定表現に置き換えることができます。
例えば、「is_not_valid
」(有効でない) の代わりに「is_invalid
」(無効である) のように表現します。
bool is_not_valid(...); void find_client(name with_the_name, bool& not_found);これは次のように再定義する必要があります。
bool is_valid(...); void find_client(name with_the_name, bool& found);
複数の操作の意図する目的が同じである場合、同義語を探すよりも多重定義を使用します。 これにより、システム内の操作の概念とバリエーションの数が最小限になり、システム全体の複雑さが軽減されます。
演算子を多重定義するときは、演算子のセマンティクスが変わらないようにします。 演算子の従来の意味を保つことができない場合、演算子を多重定義するよりも関数の別の名前を選択します。
一意性を強調する、またはこのエンティティーがアクションの主対象であることを示すには、
オブジェクト名またはパラメーター名に接頭辞「the
」または「this
」を付けます。
2 次的、一時的、補助的なオブジェクトを示すには、接頭辞「a
」または「current
」を付けます。
void change_name( subscriber& the_subscriber, const subscriber::name new_name) { ... the_subscriber.name = new_name; ... } void update(subscriber_list& the_list, const subscriber::identification with_id, structure& on_structure, const value for_value); void change(object& the_object, const object using_object);
例外はエラー状況を処理する目的にのみ使用しなければならないため、次のような、否定的な概念が明確に伝わる名詞または名詞句を使用します。
Overflow (オーバーフロー)、Threshold_Exceeded (しきい値超過)、Bad_Initial_Value (不正な初期値)
error (エラー) や exception (例外) などの語を機械的に当てはめるだけでは具体的な情報が伝わらないため、名前の一部として、プロジェクトで合意されたリストの中から bad (悪い)、incomplete (不完全)、invalid (無効)、wrong (誤り)、missing (不足)、illegal (不正) のような単語を 1 つ選んで使用します。
浮動小数点数のリテラル内の文字「E」と 16 進数の桁数字の「A」~「F」は常に大文字で表記します。
この章では、C++ の各種の宣言の使用法と形式についてのガイダンスを示します。
C++ 言語に名前空間機能が取り入れられるまで、名前のスコープを管理する手段には限られたものしかありませんでした。 結果として、グローバル名前空間の氾濫によって競合が発生し、 ライブラリーの組み合わせによっては同じプログラム内で共に使用できない、という事態を招いていました。新しい言語機能である名前空間は、 グローバル名前空間の混雑の問題を解決します。
これは、名前空間名だけをグローバルにしてもよい、という意味です。 ほかのすべての宣言は何らかの名前空間のスコープの内部で行います。
この規則を無視すると、次第に名前の競合が発生しやすくなります。
クラス以外の機能性 (クラス・カテゴリーなど) の論理的グループ化、 または、ライブラリーやサブシステムのようにクラスよりも範囲の広い機能性については、 名前空間を使用して宣言を論理的に統一します (「名前空間を使用して潜在的なグローバル名をサブシステムまたはライブラリー別に分割します」を参照)。
名前の中で、機能性の論理的グループ化を表現します。
namespace transport_layer_interface { /* ... */ }; namespace math_definitions { /* ... */ };
グローバル/名前空間スコープのデータの使用は、カプセル化の原則に反します。
クラスは C++ における基本的な設計と実装の単位です。クラスは領域と設計の抽象概念を把握する目的で、 抽象データ型 (Abstract Data Types: ADT) を実装するためのカプセル化メカニズムとして使用するべきものです。
struct
ではなく class
を使用しますクラス (抽象データ型) の実装には、struct
ではなく class
クラス・キーを使用します。
struct
クラス・キーは、C における旧式のプレーン・データ構造 (plain-old-data-structures: POD) の定義、
特に C のコードとの連携時に使用します。
class
と struct
は等価であり互いに置き換えて使用できますが、class
ではよりカプセル化に適するように、
優先的なデフォルトのアクセス制御として private が強調されています。
一貫した実践原則に従って class
と struct
を
使い分けることにより、語規則を超えた意味上の区別が可能になります。class
は
抽象概念を把握し、カプセル化を行うために最も適した構成要素です。
一方で struct
は、複数のプログラミング言語が混在したプログラム内で
交換可能な純粋なデータ構造です。
クラス宣言内のアクセス指定子は public、protected、private の順に出現することが推奨されます。
public、protected、private の順序でメンバーを宣言することにより、クラスのユーザーには最も必要な情報が最初に提示され、無関係な情報や実装の詳細がユーザーの意に反して目に入ることが少なくなります。
public または protected のデータ・メンバーを使用すると、クラスのカプセル化が抑制され、 変更に対するシステムの回復力に影響します。public のデータ・メンバーはクラスの実装をそのユーザーに公開します。protected の データ・メンバーはクラスの実装をその派生クラスに公開します。クラスの public または protected のデータ・メンバーへの変更はすべて、 結果としてユーザーと派生クラスに影響します。
このガイドラインは、初見の読者にとってはすぐには理解しにくいものと考えられます。friendship は、クラスの private 部を friend に公開しますが、 これによってカプセル化が維持される仕組みは次のとおりです。各クラスが相互依存しており、互いの内部情報を必要とする状況では、 クラスのインターフェースを介して内部の詳細をエクスポートするよりも、friendship を付与するほうが手段として優れています。
public メンバーとして内部の詳細をエクスポートすると、 クラスのクライアントにアクセス権を与えることになり望ましくありません。protected メンバーをエクスポートすると、 子孫になる可能性のある対象にアクセス権が与えられ、階層的設計が促進されることになりこれも望ましくありません。 friendship は、サブクラス化の制約を強制することなく選択的に private 部へのアクセス権を付与するしくみであるため、 その部分へのアクセスを必要とする以外のすべての対象からのカプセル化が維持されます。
friendship を使用してカプセル化を維持することのよい例として、friend テスト・クラスに friendship を付与することを考えます。friend テスト・クラスは、 クラスの内部を見ることによって適切なテスト・コードを実装できますが、 その後、納入されるコードから friend のテスト・クラスを削除できます。つまり、カプセル化の作用は損なわれず、 納入されるコードに friend のコードが混じることもありません。
クラス宣言には 1 つの関数宣言だけが含まれるべきであり、関数の定義 (実装) を含めてはなりません。
クラス宣言の中に関数の定義を置くと、クラスの仕様に実装の詳細が混ざることになります。 これにより、クラスのインターフェースの識別と解読が難しくなり、コンパイル時の依存性も増加します。
クラス宣言の中で関数を定義すると、関数のインライン化の統制もとりにくくなります (「No_Inline
条件付きコンパイル・シンボルを使用して、
インライン・コンパイルを無効にします」も参照)。
配列または STL コンテナー内でクラスを使用できるようにするには、クラスが public のデフォルト・コンストラクターを備えているか、コンパイラーによるデフォルト・コンストラクターの生成を許可することが必要です。
この規則の例外はクラスが参照型の非静的データ・メンバーを持つときで、その場合、 意味のあるデフォルト・コンストラクターの作成はしばしば不可能になります。したがって、 オブジェクトのデータ・メンバーへの参照を使用することには問題があります。
必要であり明示的に宣言されない場合、コンパイラーは暗黙的に、クラスに対してコピー・コンストラクターと代入演算子を生成します。コンパイラーによって 定義されたコピー・コンストラクターと代入演算子は、 メンバーのコピーを明示的に実装します。これは Smalltalk の用語で一般に 「シャロー・コピー」と呼ばれるもので、ポインターがビット単位でコピーされます。 コンパイラーによって生成されるコピー・コンストラクターとデフォルト代入演算子を使用すると、確実にメモリー・リークが発生します。
// Adapted from [Meyers, 92]. void f() { String hello("Hello"); // Assume String is implemented // with a pointer to a char // array. { // Enter new scope (block) String world("World"); world = hello; // Assignment loses world's // original memory } // Destruct world upon exit from // block; // also indirectly hello String hello2 = hello; // Assign destructed hello to // hello2 }
このコードでは、文字列「World
」を保持するメモリーは代入後に失われます。内側のブロックの終了時点で world
は破棄され、hello
によって参照されるメモリーも同時に失われます。
破棄された hello
が hello2
に代入されます。
// Adapted from [Meyers, 1992]. void foo(String bar) {}; void f() { String lost = "String that will be lost!"; foo(lost); }
このコードでは、引数 lost
を指定して foo
が呼び出されると、
コンパイラーによって定義されたコピー・コンストラクターを使用して lost
が foo
にコピーされます。
lost
は String that will be lost!
へのポインターのビット単位のコピーと共にコピーされるため、foo
の終了時点で、lost
のコピーは String that will be lost!
を保持するメモリーと共に破棄されます。
これは、正しくメモリーを解放するようにデストラクターが実装されていることを想定した動作です。
// Example from [X3J16, 95; section 12.8] class X { public: X(const X&, int); // int parameter is not // initialized // No user-declared copy constructor, thus // compiler implicitly declares one. }; // Deferred initialization of the int parameter mutates // constructor into a copy constructor. // X::X(const X& x, int i = 0) { ... }
クラス宣言の中に「標準の」コピー・コンストラクターのシグニチャーを見つけられないとき、コンパイラーは暗黙のうちにコピー・コンストラクターを宣言します。しかしながら、 デフォルト・パラメーターの初期化が先延ばしされたことにより、コンストラクターがコピー・コンストラクターに変化する場合があります。結果として、コピー・コンストラクターが使用されるときの状態は不確定となります。 この不確定が原因で、コピー・コンストラクターの使用形態は不正になります (参考資料 [X3J16, 95; section 12.8])。
クラスを明示的に派生不可として設計する場合を除いて、そのデストラクターは常に virtual として宣言することが推奨されます。
派生したクラス・オブジェクトの削除をポインターまたは基本クラス型への参照を介して行うと、基本クラスのデストラクターが virtual として宣言されている場合を除き、結果は未定義の動作となります。
// Bad style used for brevity class B { public: B(size_t size) { tp = new T[size]; } ~B() { delete [] tp; tp = 0; } //... private: T* tp; }; class D : public B { public: D(size_t size) : B(size) {} ~D() {} //... }; void f() { B* bp = new D(10); delete bp; // Undefined behavior due to // non-virtual base class // destructor }
explicit
指定子を付けて宣言することにより、単独のパラメーター・コンストラクターを暗黙的変換のために使用されるのを防ぐこともできます。
virtual でない関数は不変の振る舞いを実装し、 派生クラスによって特化されることを想定していません。このガイドラインに違反すると、予期しない動作が発生する場合があります。 同じオブジェクトが時によって異なった動作をする場合があります。
virtual でない関数は静的にバインドされます。そのため、オブジェクトに対して呼び出される関数は、オブジェクトの実際の型ではなく、 オブジェクトを参照する変数 (次の例ではそれぞれ A へのポインターと B へのポインター) の静的型によって支配されます。
// Adapted from [Meyers, 92]. class A { public: oid f(); // Non-virtual: statically bound }; class B : public A { public: void f(); // Non-virtual: statically bound }; void g() { B x; A* pA = &x; // Static type: pointer-to-A B* pB = &x; // Static type: pointer-to-B pA->f(); // Calls A::f pB->f(); // Calls B::f }
virtual でない関数は、特化と多態性を制限することによってサブクラスに制約を課すため、操作を非 virtual と宣言する前に、すべてのサブクラスについて操作が真に不変であることを保証しなければなりません。
オブジェクト作成の間のオブジェクトの状態の初期化は、コンストラクター内部の代入演算子によるのではなく、コンストラクター初期化子 (メンバー初期化子リスト) によって実行されるのが望ましい動作です。
class X { public: X(); private Y the_y; }; X::X() : the_y(some_y_expression) { } // // "the_y" initialized by a constructor-initializer望ましくない処理:
X::X() { the_y = some_y_expression; } // // "the_y" initialized by an assignment operator.
オブジェクト作成には、コンストラクター本体の実行に先立って、 すべての基本クラスとデータ・メンバーの作成が伴います。データ・メンバーの初期化には、 コンストラクター初期化子を使用して実行されるときは 1 つの演算子 (初期値による作成) が必要であるのに対し、 コンストラクター本体内で実行される場合は 2 つの演算子 (作成と代入) が必要です。
複雑にネストされた集約クラス (クラスを含むクラスを含むクラス...) については、 複数の操作 (作成とメンバーの代入) に伴う性能オーバーヘッドがかなり大きくなる可能性があります。
class A { public: A(int an_int); }; class B : public A { public: int f(); B(); }; B::B() : A(f()) {} // undefined: calls member function but A bas // not yet been initialized [X3J16, 95].
基本クラスのすべてのメンバー初期化子が完了する前にコンストラクター初期化子から直接または間接的にメンバー関数が呼び出された場合、 操作の結果は未定義です (参考資料 [X3J16, 95])。
コンストラクター内でメンバー関数を呼び出すときは注意が必要です。仮想関数が呼び出される場合であっても、実行されるのは、コンストラクターまたはデストラクターのクラス内で定義された関数、またはその基本クラスの関数です。
static const
を使用します整数のクラス定数を定義するときは、#define
の定数または
グローバル定数ではなく、static const
データ・メンバーを使用します。コンパイラーで static const
がサポートされていない場合、
代わりに enum
を使用します。
class X { static const buffer_size = 100; char buffer[buffer_size]; }; static const buffer_size;別の望ましい処理:
class C { enum { buffer_size = 100 }; char buffer[buffer_size]; };望ましくない処理:
#define BUFFER_SIZE 100 class C { char buffer[BUFFER_SIZE]; };
これにより、明示的な戻り型なしで宣言された関数に対して、戻り型がないというエラーがコンパイラーから出されたときの混乱を防ぎます。
関数の宣言と定義の両方で同じ名前を使用します。これは最小限の意外性のための対策です。パラメーター名を与えると、コードがより整理されて読みやすくなります。
関数の本体内にいくつもの return ステートメントを置くことは goto
文の多用と同様に、コードの解読と保守を困難にします。
複数の return ステートメントの存在は、非常に小さな関数の中で、すべての return
を同時に見渡すことができ、
コードの構造が非常に規則的である場合にのみ許容されます。
type_t foo() { if (this_condition) return this_value; else return some_other_value; }
戻り型が void の関数には、return 文を含めないようにします。
グローバルな副次作用 (グローバル・データや名前空間データなど、 関数の内部オブジェクト状態以外の非公開のデータを変更する) を生み出す関数はできるだけ作成しないことが推奨されます (「グローバル/名前空間スコープのデータをできるだけ使用しないようにします」も参照)。 必要上やむを得ず作成する場合、関数仕様の一部としてすべての副次作用を明確に文書化します。
必要なオブジェクトをパラメーターとして渡すようにすると、実行状況へのコードの依存性が低下し、コードがより堅牢になり、理解しやすくなります。
呼び出す側の視点で考えた場合、パラメーター宣言の順序 は重要です。
このような定義順により、関数呼び出しでデフォルト値を利用して引数の数を減らすことができるようになります。
パラメーターの個数が一定しない関数の引数については、型チェックを実行できません。
forward 宣言は別として、関数の再宣言時にデフォルト値を関数に追加しないようにします。 関数は一度だけ宣言することが推奨されます。このガイドラインに違反すると、 後続の宣言を認識していないコードの読み手にとって混乱の原因となる場合があります。
不変の振る舞い (定数値を返す、定数の引数を受け取る、副次作用を伴わず動作する) が関数に含まれているかどうかをチェックし、そのような振る舞いがあれば、const
指定子を使用して振る舞いを強調します。
const T f(...); // Function returning a constant // object. T f(T* const arg); // Function taking a constant // pointer. // The pointed-to object can be // changed but not the pointer. T f(const T* arg); // Function taking a pointer to, and T f(const T& arg); // function taking a reference to a // constant object. The pointer can // change but not the pointed-to // object. T f(const T* const arg); // Function taking a constant // pointer to a constant object. // Neither the pointer nor pointed- // to object may change. T f(...) const; // Function without side-effect: // does not change its object state; // so can be applied to constant // objects.
値によりオブジェクトを渡したり戻したりすることは、 コンストラクターとデストラクターに重いオーバーヘッドを課す場合があります。オブジェクトを参照によって渡したり戻したりすることで、 コンストラクターとデストラクターのオーバーヘッドを回避できます。
参照によって渡される引数を変更できないことを示すために、Const
参照を使用できます。典型的な使用例は、コピー・コンストラクターと代入演算子です。
C::C(const C& aC); C& C::operator=(const C& aC);
次の例を考慮します。
the_class the_class::return_by_value(the_class a_copy) { return a_copy; } the_class an_object; return_by_value(an_object);
引数として an_object
を指定して return_by_value
が
呼び出されると、the_class
コピー・コンストラクターが
呼び出され、an_object
が a_copy
にコピーされます。the_class
コピー・コンストラクターが
再び呼び出され、a_copy
が関数の戻り一時オブジェクトにコピーされます。関数からの戻りの時に、the_class
デストラクターが
呼び出され a_copy
が破棄されます。その後、the_class
デストラクターが再び呼び出され、return_by_value
によって返されたオブジェクトを
破棄します。このような何もしない関数呼び出しの全体コストは、2 つのコンストラクターと 2 つのデストラクターです。
the_class
が派生クラスであり、ほかのクラスのメンバー・データを含んでいた場合、
状況はさらに不利になります。基本クラスと、包含されるクラスのコンストラクターとデストラクターも呼び出され、
関数呼び出しに伴うコンストラクターとデストラクターの呼び出し回数が増加します。
このガイドラインは、常に参照によってオブジェクトを渡し、戻すことを開発者に推奨しているものと思われるかもしれませんが、 オブジェクトが必要なときに、ローカルのオブジェクトへの参照または参照への参照を返さないように注意する必要もあります。ローカル・オブジェクトへの参照を返すと問題の原因となります。 これは、関数からの復帰時に、返される参照が破棄されたオブジェクトにバインドされるからです。
ローカル・オブジェクトは関数のスコープを離れる時点で破棄されます。破棄されたオブジェクトを使用すると問題の原因になります。
このガイドラインに違反すると、メモリー・リークの原因となります。
class C { public: ... friend C& operator+( const C& left, const C& right); }; C& operator+(const C& left, const C& right) { C* new_c = new C(left..., right...); return *new_c; } C a, b, c, d; C sum; sum = a + b + c + d;
sum の計算時に operator+ の中間的な結果は格納されないため、中間オブジェクトを削除できず、メモリー・リークの原因となります。
このガイドラインへの違反はデータのカプセル化の原則に違反し、想定外の悪影響につながる場合があります。
#define
ではなくインライン関数を使用しますただし、インライン化は慎重に使用し、対象をごく小さな関数に限定してください。大きな関数をインライン化するとコードの膨張の原因になる場合があります。
インライン関数の実装をクライアント・コードのコンパイルでも利用可能にする必要があるため、 関数をインライン化するとモジュール間のコンパイル時依存性も複雑化します。
参考資料 [Meyers, 1992] では、マクロの不適切な使用法について、 以下のやや極端な例を挙げて詳細に考察しています。
望ましくない処理:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
望ましい処理:
inline int max(int a, int b) { return a > b ? a : b; }
マクロ MAX
には多くの問題があります。このマクロは型が安全でなく、その動作は一定ではありません。
int a = 1, b = 0; MAX(a++, b); // a is incremented twice MAX(a++, b+10); // a is incremented once MAX(a, "Hello"); // comparing ints and pointers
1 つのアルゴリズムを利用でき、そのアルゴリズムを少数のパラメーターによって数値化できるときは、 関数の多重定義よりもデフォルト・パラメーターを使用します。
デフォルト・パラメーターを使用すると、多重定義される関数を減らすのに役立ち、保守容易性が向上し、 関数呼び出しで必要な引数の数が減り、コードの読みやすさが改善されます。
同じ意味の操作に対して、引数の型が異なる複数の実装が必要なときは、 関数の多重定義を使用します。
演算子を多重定義するときは、従来の意味を保つようにします。関連する演算子 (例えば、operator==
や operator!=
) を忘れずに定義してください。
1 つの整数値の引数を取る関数によって、1 つのポインター引数を取る関数を多重定義することは避けます。
void f(char* p); void f(int i);
次の呼び出しは想定外の事態を引き起こす場合があります。
f(NULL); f(0);
多重定義の解決は f(char*)
ではなく f(int)
に解決します。
operator=
が *this
への参照を返すようにしますC++ では、代入演算子の連鎖が可能です。
String x, y, z; x = y = z = "A string";
代入演算子は右結合であるため、文字列「A string
」は z に代入され、
続いて z から y、y から x へと代入されます。operator=
は実際には、= の右辺の各式に対して、右から左の順で 1 回ずつ呼び出されます。
これは、各 operator=
の結果がオブジェクトであることも意味しますが、
左辺オブジェクトまたは右辺オブジェクトのどちらを返すかの選択が可能です。
推奨される実践原則では、代入演算子のシグニチャーは常に次の形式であるべき、と述べられています。
C& C::operator=(const C&);
左辺のオブジェクトだけが候補である (rhs は const 参照、lhs は非 const 参照) ため、*this
が返されるべきです。詳しい議論については、
参考資料 [Meyers, 1992] を参照してください。
operator=
で自己代入をチェックするようにしますチェックを実行することが望ましい理由は 2 つあります。 まず、派生クラスのオブジェクトの代入には、継承階層の 1 つ上の各基本クラスの代入演算子の呼び出しが伴い、 これらの操作を省略することにより実行時間が大幅に短縮される場合があります。 2 番目の理由として、代入には「rvalue」オブジェクトのコピーに先立って「lvalue」オブジェクトの破棄が伴います。 自己代入の場合、rvalue オブジェクトはそれが代入される前に破棄されるため、代入の結果は未定義です。
コードが 60 行を超えるような、極端に長い関数を記述しないでください。
return 文の数を最小限にします。1 つだけにするのが理想的です。
循環的複雑度を 10 未満 (決定ステートメント + 1 の合計、終了ステートメントが 1 つの関数の場合) に抑えるように努めます。
拡張された循環的複雑度を 15 未満 (決定ステートメント + 論理演算子 + 1 の合計、 終了ステートメントが 1 つの関数の場合) に抑えるように努めます。
参照の平均最大スパン (ローカル・オブジェクトの宣言からその使用の最初のインスタンスまでの行数で測った長さ) をできるだけ短くします。
大規模なプロジェクトでは通常、システム全体で頻繁に使用される型のコレクションが存在します。 この場合、これらの型を 1 つまたは複数の下位レベルのグローバル・ユーティリティー名前空間に集めるのがよい方法です (「基本型を使用しません」の例を参照)。
高い移植性を目標とするとき、数値オブジェクトによって占められるメモリー空間の管理が必要なとき、 または、特定の範囲の値が必要なときは、基本型を使用するべきではありません。このような状況では、 適切な基本型を使用して、サイズ制約を伴う明示的な型名を宣言するのがより優れた方法です。
ループ・カウンター、配列索引などを通じて基本型がコードに入り込まないよう常に注意します。
namespace system_types { typedef unsigned char byte; typedef short int integer16; // 16-bit signed integer typedef int integer32; // 32-bit signed integer typedef unsigned short int natural16; // 16-bit unsigned integer typedef unsigned int natural32; // 32-bit unsigned integer ... }
基本型の表現は実装に依存します。
typedef
を使用して同義語を作成しローカルな意味を強化しますtypedef
を使用して既存の名前の同義語を作成し、
より意味のあるローカルな名前を与えて読みやすさを改善します (これを行うことによる実行時のペナルティーはありません)。
typedef
は、修飾名の略記を定める目的にも使用できます。
// vector declaration from standard library // namespace std { template <class T, class Alloc = allocator> class vector { public: typedef typename Alloc::types<T>reference reference; typedef typename Alloc::types<T>const_reference const_reference; typedef typename Alloc::types<T>pointer iterator; typedef typename Alloc::types<T>const_pointer const_iterator; ... } }
typedef
によって作成される typedef 名を使用するときは、コードの同じ断片の中で、元の名前と同義語を混用しないようにします。
名前付き定数を優先的に使用します。
代わりに const
または enum
を使用します。
望ましくない処理:
#define LIGHT_SPEED 3E8望ましい処理:
const int light_speed = 3E8;サイズ可変配列の場合の望ましい処理:
enum { small_buffer_size = 100, large_buffer_size = 1000 };
#define
によって導入される名前がコンパイルの前処理の間に置換され、シンボル・テーブルに出現しないため、デバッグがより難しくなります。
extern
として宣言されない const
オブジェクトには内部リンケージがあり、宣言時にこれらの定数オブジェクトを初期化することにより、コンパイル時に初期化子を使用できるようになります。
定数オブジェクトは読み取り専用メモリー内に存在してもかまいません。
オブジェクトが自己初期化を行わない場合、オブジェクト定義に初期値を指定します。意味のある初期値を割り当てることができない場合、 「nil」値を割り当てるか、後でオブジェクトを宣言することを検討します。
大きなオブジェクトについては一般に、オブジェクトの作成後に代入によってオブジェクトを初期化することは、 非常にコストがかかるので望ましくありません (「コンストラクター内での代入ではなくコンストラクター初期化子を使用します」も参照)。
作成時にオブジェクトの適切な初期化ができない場合、 「初期化されていない」ことを意味する標準的な「nil」値を使用してオブジェクトを初期化します。nil 値を使用するのは、 アルゴリズムによって統制のとれた方法で排除可能な「使用できない既知の値」を宣言する目的で、 適切な初期化の前にオブジェクトが使用されたときに変数未初期化エラーを発生させる目的で行う初期化だけが対象です。
一部の型、特に角度などのモジュロ型に対しては nil 値を宣言できない場合もあることに注意してください。そのような場合、 最も可能性の低い値を選択します。
この章では、C++ のさまざまな式とステートメントの使い方と形式に関するガイダンスを示します。
式のネストのレベルとは、演算子の優先順位の規則を無視するなら、式を左から右に評価するために必要な、ネストされた括弧の組の数と定義されます。
ネストのレベルが多すぎると、式がわかりにくくなります。
演算子によって評価順序が指定されているのでないかぎり (コンマ演算子、3 進法、論理積、論理和)、式が特定の順序で評価されるものと仮定してはなりません。 仮定をすると、予想外の結果が発生し、移植性が失われるおそれがあります。
例えば、同じ式の中で、変数のインクリメントまたはデクリメントと、その変数の使用を一緒に行ってはなりません。
foo(i, i++); array[i] = i--;
NULL
ではなく 0 を使用するようにしますNULL ポインターに対して 0 と NULL
のどちらを使用するかは、非常に意見の分かれる問題です。
C と C++ のどちらにおいても、値が 0 になる任意の定数式は NULL ポインターとして解釈できると規定されています。0 は見間違えやすく、
リテラルは極力使用しないように言われているので、従来、
プログラマーは NULL ポインターとして NULL
マクロを使用してきました。残念ながら、NULL
に対する移植可能な定義はありません。
ANSI C コンパイラーの中には (void *)0 を使用するものがありますが、C++ に対しては使用できません。
char* cp = (void*)0; /* Legal C but not C++ */
したがって、単なるゼロではなく (T*)0 の形式での NULL
の定義は
すべて、C++ ではキャストする必要があります。従来、NULL ポインターに対して 0 の使用を提唱しているガイドラインは、
キャストの必要性を軽減し、コードの移植性を高めることを意図してました。しかし、多くの C++ 開発者は、0 より NULL
を使用する方を好み、
現在ではほとんどのコンパイラー (より正確には、ほとんどのヘッダー・ファイル) が NULL
を 0 として実装していると主張しています。
このガイドラインでは、0 を使用するように規定してあります。
これは、0 は NULL
の値にかかわらず機能することが保証されているためです。
ただし、異なる意見もあるので、この規定はヒントのレベルとし、必要に応じて守っても無視してもよいものとします。
古い形式のキャストではなく、新しいキャスト演算子 (dynamic_cast、static_cast、reinterpret_cast、const_cast
) を使用するようにします。
新しいキャスト演算子がない場合は、キャストをまったく行わないようにします。特に、 ダウンキャスト (基本クラスのオブジェクトから派生クラスのオブジェクトへの変換) は使用しないようにします。
キャスト演算子の使用法は以下のとおりです。
dynamic_cast
- 実行時の型情報を使用して、
同じクラス階層 (サブ型) のメンバー間でキャストを行います (実行時の型情報は仮想関数によって入手できます)。このようなクラス間でのキャストは、
安全であることが保証されています。static_cast
- 実行時の型情報を使用しないで同じクラス階層のメンバー間でキャストを行います。そのため、安全性は保証されません。プログラマーが型の安全を保証できない場合は、dynamic_cast
を使用するようにします。reinterpret_cast
- 関連のないポインター型と整数型の間でキャストを行います。安全ではないので、示されている型の間でのみ使用するようにしなければなりません。
const_cast
-const
パラメーターとして指定されている関数引数の 「定数性」を取り除きます。const_cast
は const オブジェクトとして実際に定義されているオブジェクト (読み取り専用メモリーに存在する可能性がある) の「定数性」を取り除くことを意図するものではないことに注意してください。
typeid
を使用して型変換のロジックを実装してはいけません。キャスト演算子によって自動的に行われる型のチェックと変換を使用するようにします。詳しくは、参考資料 [Stroustrup, 1994] を参照してください。
望ましくない処理:
void foo (const base& b) { if (typeid(b) == typeid(derived1)) { do_derived1_stuff(); else if (typeid(b) == typeid(derived2)) { do_derived2_stuff(); else if () { } }
古い形式のキャストに対して型システムは有効ではないので、コンパイラーで見つからない検出困難なバグが発生するおそれがあります。 メモリー管理システムが壊れたり、仮想関数テーブルが壊れたり、 派生クラスのオブジェクトとしてオブジェクトにアクセスすると関係のないオブジェクトが損傷したりする可能性があります。存在しないポインターまたはフィールドが参照される可能性があるため、 読み取りアクセスであっても被害が発生する場合があることに注意してください。
新しい形式のキャスト演算子では、型変換がより安全 (ほとんどの場合) で、より明示的になっています。
古い形式の論理型マクロまたは定数を使用しないでください。標準的な論理型の値 true はありません。代わりに新しい bool
型を使用するようにします。
従来、true (1 または ! 0) に対する標準的な値はないので、非ゼロの式と true を比較すると失敗する場合があります。
代わりに真偽値式を使用します。
避けるべき処理:
if (someNonZeroExpression == true) // May not evaluate to true望ましい処理:
if (someNonZeroExpression) // Always evaluates as a true condition.
このような比較の結果は、ほとんどの場合意味のないものです。
削除したオブジェクトに対するポインターに NULL を設定することで障害の発生を防ぎます。 非 NULL ポインターで削除を繰り返すと危険ですが、NULL ポインターで削除を繰り返しても危険はありません。
新しいコードを後で追加する可能性があるので、削除を行った後は、関数から返る前であっても、必ず NULL ポインター値を代入するようにします。
不連続な値で分岐するときは switch ステートメントを使用します
分岐条件が不連続な値の場合は、一連の else if ではなく switch ステートメントを使用します。
default
分岐を設けますswitch ステートメントでは default
分岐を必ず使用し、
エラーを検出するために default
分岐を使用する必要があります。
このポリシーに従えば、新しいスイッチの値を追加し、新しい値を処理するための分岐を追加し忘れたときでも、 既存の default 分岐がエラーを検出します。
反復とループの終了がループ・カウンターに基づく場合は、while ステートメントより for ステートメントを使用します。
ループ終了条件以外の方法 (break
、return
、goto
を使用して) でループを終了しないようにします。
また、continue
を使用してループの途中から次の反復にスキップすることも避けます。この規則に従えば、制御パスのフローの数が減り、
コードを理解しやすくなります。
goto
ステートメントを使用してはなりませんこれは普遍的なガイドラインのようです。
このようなコードは読む者を混乱させ、保守における潜在的なリスクになるおそれがあります。
この章では、メモリー管理とエラー通知に関するガイドラインを示します。
C ライブラリーの関数 malloc
、calloc
、realloc
を使用してオブジェクト・スペースを割り当ててはなりません。
この目的には C++ の演算子 new を使用するようにします。
C の関数を使用してメモリーを割り当てなければならないのは、破棄のためにメモリーを C ライブラリー関数に渡す場合だけです。
C の関数によって割り当てられたメモリーを解放するために、または new によって作成されたオブジェクトでの解放のために delete を使用してはなりません。
空の角括弧 ([]) 表記を付けない配列オブジェクトに対して delete を使用すると、配列の最初の要素だけが削除されるため、メモリー・リークが発生します。
C++ の例外メカニズムの使用に関してはそれほど多くの経験が得られていないので、 ここで示すガイドラインは将来大幅に変更される可能性があります。
C++ のドラフト標準では、エラーについて論理エラーとランタイム・エラーという 2 種類の 大きなカテゴリーが定義されています。論理エラーは、避けることのできるプログラミング・エラーです。ランタイム・エラーは、 プログラムのスコープを超えたイベントによるエラーと定義されます。
例外の使用に関する一般的なルールは、通常状態のシステムや、過負荷やハードウェア障害の発生していないシステムでは例外が発生してはならない、というものです。
開発中は関数の事前条件と事後条件のアサーションを使用して、水際でエラー検出を行うようにします。
最終的なエラー処理コードを実装するまでは、 アサーションが簡単で便利な暫定的エラー検出メカニズムを提供します。アサーションには、「NDEBUG」プリプロセッサー・シンボルを使用してコンパイル時に無効にできるという利点もあります (「特定の値を使用して NDEBUG シンボルを定義します」を参照)。
これまでこの目的のためには assert マクロが使用されてきましたが、 参考資料 [Stroustrup, 1994] では次に示すような代わりのテンプレートが提供されています。
template<class T, class Exception> inline void assert ( T a_boolean_expression, Exception the_exception) { if (! NDEBUG) if (! a_boolean_expression) throw the_exception; }
頻繁に発生する予想されるイベントに対して例外を使用してはなりません。 例外は、コードの通常の制御フローを乱し、コードの理解や保守を困難にします。
予想されるイベントは、コードの通常の制御フローで処理する必要があります。 必要に応じて、関数の戻り値や「out」パラメーターのステータス・コードを使用します。
また、制御構造を実装するために例外を使用してはなりません。これは、「goto」ステートメントと同じ形式です。
これにより、すべての例外が最低限の共通の操作セットをサポートするようになり、 上位レベルの少数のハンドラー群で処理できるようになります。
論理エラー (ドメイン・エラー、無効引数エラー、長さエラー、範囲外エラー) は、アプリケーション・ドメイン・エラー、 関数呼び出しに渡された無効な引数、許容サイズを超えたオブジェクトの作成、許容範囲外の引数値を示すために使用する必要があります。
ランタイム・エラー (範囲エラーとオーバーフロー・エラー) は、算術エラーと設定エラー、壊れたデータ、 実行時にだけ検出されるリソース不足エラーを示すために使用する必要があります。
大規模システムでは、各レベルで多数の例外を処理する必要があるため、 コードの可読性や保守容易性が低下します。例外処理によって、通常の処理が妨げられる場合もあります。
例外の数を最低限にする方法は、以下のとおりです。
少数の例外カテゴリーを使用して、抽象レベル間で例外を共有します。
標準例外から派生する特殊化された例外では、より一般的な例外を扱うようにします。
オブジェクトに「例外的な」状態を追加し、オブジェクトの有効性を明示的にチェックするための基本要素を用意します。
例外を発生させる関数 (例外を渡すだけの関数でなく) は、送出されるすべての例外を例外仕様で宣言する必要があります。クライアントに警告しないで黙って例外を発生させてはなりません。
開発時には、適切なロギング・メカニズムを使用して、できるかぎり早く例外を通知します。これには、「スローポイント」が含まれます。
到達不能なハンドラーをコーディングすることがないよう、例外ハンドラーは最も派生度の高いクラスから最も基本的なクラスの順序で定義する必要があります。 後に示す悪い例を参照してください。また、ハンドラーは宣言の順序で照合されるので、このように宣言することにより、 最も適切なハンドラーが例外を捕らえるようになります。
望ましくない処理:
class base { ... }; class derived : public base { ... }; ... try { ... throw derived(...); // // Throw a derived class exception } catch (base& a_base_failure) // // But base class handler "catches" because // it matches first! { ... } catch (derived& a_derived_failure) // // This handler is unreachable! { ... }
例外を送出し直すのでないかぎり、catch-all 例外ハンドラー (... を使用して宣言されているハンドラー) は避けます。
catch-all ハンドラーは、ローカルな内部処理のためだけに使用します。この場合には、 そのレベルでは例外を処理できないという事実が隠されてしまわないようにするため、例外を送出し直す必要があります。
try { ... } catch (...) { if (io.is_open(local_file)) { io.close(local_file); } throw; }
関数のパラメーターとしてステータス・コードを返すときは、 必ず関数本体の最初の実行可能ステートメントとしてパラメーターに値を代入します。 規則的に、すべてのステータスをデフォルトで成功、またはデフォルトで失敗にします。例外ハンドラーを含めて、関数からのすべての出口について考慮します。
不適切な入力に対して関数が誤った出力を生成する可能性がある場合は、 制御された方法で無効な入力を検出して通知するコードを関数内に設ける必要があります。クライアントに適切な値を渡すように、 コメントで指示することはあてになりません。遅かれ早かれコメントが無視されるようになるのはほぼ確実であり、 無効なパラメーターが検出されないとデバッグの困難なエラーが発生します。
この章では、結果として 移植性を損なう言語機能について説明します。
パス名はオペレーティング・システム間で標準の表現方法がありません。 パス名を使用すると、プラットフォーム依存を招きます。
#include "somePath/filename.hh" // Unix #include "somePath\filename.hh" // MSDOS
型の表現と配置はコンピューターのアーキテクチャーに強く依存します。表現と配置について推測に基づいて作業を進めると予期しない結果を招き、 移植性が損なわれます。
特に、long またはそのほかのすべての数値型は移植性が非常に低いため、int
のポインターに使用することは避けます。
特定のアンダーフローまたはオーバーフローの振る舞いに依存しません
汎用的な定数を使用して、ワード規模での変動による問題の発生を防ぎます。
const int all_ones = ~0; const int last_3_bits = ~0x7;
コンピューターのアーキテクチャーによって特定の型の配置が決まります。緩やかな配置要求を持つ型をより厳しい配置要求の型に変換すると、 プログラムに障害が発生する可能性があります。
この章では、C++ コードの再利用について説明します。
標準のライブラリーを利用できない場合、標準のライブラリー・インターフェースに基づいてクラスを作成します。これにより将来の移行が容易になります。
振る舞いが特定のデータ型に依存しない場合は、テンプレートを使用して振る舞いを再利用します。
public
継承を使用して "isa" 関係を表し、基本クラス・インターフェースと、必要によりその実装を再利用します。
実装またはモデリングの「parts/whole」関係を再利用する場合は、private 継承の使用を避けます。再定義を行わずに実装を再利用するには、private 継承の代わりに包含関係の利用が最適です。
private 継承は、基本クラス操作の再定義が必要な場合に使用します。
多重継承は複雑さが増すため、その使用は慎重に行う必要があります。名前の多義性と繰り返し継承による複雑さについては、 参考資料 [Meyers, 1992] で詳細に説明しています。複雑さは次のような場合に発生します。
あいまい性から発生。複数のクラスで同じ名前を使用する場合、 名前に対して修飾子が付かない参照は本質的にあいまいになります。あいまい性は、メンバー名をそのメンバーのクラス名で修飾すると解決できます。ただし、 この方法は多態性が無効になり、仮想関数が静的結合の関数に変わる欠点もあります。
同じ基本から複数の一連のデータ・メンバーの継承を繰り返すと (継承階層の異なるパスを経由した派生クラスで基本クラスを複数回継承した場合)、 複数の一連のデータ・メンバーのどれを使用するかが決められなくなる問題が発生します。
複数回継承されたデータ・メンバーは、仮想継承 (仮想基本クラスの継承) を使用すると防ぐことができます。次の理由から常に仮想継承を使用するわけではありません。仮想継承は、 基本となるオブジェクト表現が変わり、アクセスの効率が低下する欠点があります。
すべての継承を仮想にする必要のあるポリシーを決定し、同時にすべての包括的な空間と時間のペナルティーを科すと、 権限が過剰になります。
このため、多重継承については、クラスの設計者は仮想継承と非仮想継承のいずれを使用するかの決定を可能にするために、 クラスの将来の使用に対する洞察力を持つ必要があります。
この章では、コンパイルの問題について説明します。
モジュールの実装でのみ必要なほかのヘッダー・ファイルをモジュール仕様に組み込まないようにします。
ポインターまたは参照の可視性のみが必要な場合は、ほかのクラスに対する可視性を得る目的で仕様にヘッダー・ファイルを組み込むことを避けます。 代わりに forward 宣言を使用します。
// Module A specification, contained in file "A.hh" #include "B.hh" // Don't include when only required by // the implementation. #include "C.hh" // Don't include when only required by // reference; use a forward declaration instead. class C; class A { C* a_c_by_reference; // Has-a by reference. }; // End of "A.hh"
コンパイルへの依存性の最小化を行うことは、さまざまな名前の一定の設計法、 または設計パターンの根拠になります。Handle、Envelope (参考資料 [Meyers, 1992])、Bridge ([Gamma]) クラス などです。クラス抽象化の責務を 2 つの関連クラスに分割することにより、1 つがクラスのインターフェースを提供し、 もう一方が実装を行います。クラスとのそのクライアント間の依存性は最小になります。 実装をどんなに変更しても (実装クラス)、クライアントの再コンパイルは必要なくなります。
// Module A specification, contained in file "A.hh" class A_implementation; class A { A_implementation* the_implementation; }; // End of "A.hh"
この方法を使用すると、インターフェース・クラスと実装クラスを 2 つの別のクラス階層として特化することもできます。
NDEBUG
シンボルを定義しますNDEBUG
シンボルは、従来 assert マクロを使用して実装するアサーション・コードをコンパイルで取り去ることに使用されました。従来の使用方法では、アサーションを除外する場合にこのシンボルを定義しました。
ただし、開発者はアサーションの存在に気が付かない場合があり、このためこのシンボルは定義されませんでした。
assert のテンプレート版の使用をお勧めします。この場合、アサーション・コードが必要なときは NDEBUG
シンボルに明示的な値 0 を指定します。
除外する場合は 0 以外を指定します。NDEBUG
シンボルに特定の値を指定しないで後に続くアサーション・コードをコンパイルすると、
コンパイル・エラーが発生します。このため、開発者はアサーション・コードの存在に気が付きます。
このマニュアルで説明したすべてのガイドラインのまとめを次に示します。
常識的な考え方を採用します
常に #include
を使用してモジュールの仕様にアクセスします
1 つまたは複数のアンダースコアー (「_」) で始まる名前を宣言しないようにします
グローバル宣言を名前空間だけに限定します
明示的に宣言されるコンストラクターを持つクラスに対して常にデフォルト・コンストラクターを与えます
ポインター型のデータ・メンバーを持つクラスに対して常にコピー・コンストラクターと代入演算子を宣言します
デフォルト値を与える目的でコンストラクター・パラメーターを再宣言することはしません
デストラクターを常に virtual として宣言します
非 virtual 関数を再定義しません
コンストラクター初期化子からメンバー関数を呼び出しません
ローカル・オブジェクトへの参照を返しません
new で初期化される、非参照ポインターを返しません
メンバー・データへ非 const 参照またはポインターを返しません
operator=
が *this
への参照を返すようにします
operator=
で自己代入をチェックするようにします
定数オブジェクトの「定数性」を失いません
式の評価が特定の順序で行われるものと仮定してはなりません
古い形式のキャストを使用してはなりません
真偽値式に対しては新しい bool
型を使用するようにします
論理型の値 true と直接比較してはいけません
同じ配列内にないポインターとオブジェクトを比較してはいけません
削除したオブジェクトのポインターには常に null
ポインター値を代入するようにします
エラーを検出するために、switch ステートメントには必ず default 分岐を設けます
goto ステートメントを使用してはなりません
C と C++ のメモリー操作を混在させないようにします
new によって作成された配列オブジェクトを削除するときは、必ず delete[] を使用します
ハードコードされたファイル・パス名を使用してはなりません
型の表現は仮定しません
型の配置を仮定しません
特定のアンダーフローまたはオーバーフローの振る舞いに依存しません
「shorter」型から「longer」型に変更しません
特定の値を使用して NDEBUG
シンボルを定義します
モジュールの仕様と実装を個別のファイルに配置します
ファイル名拡張子のセットを 1 つ選択して、ヘッダー・ファイルと実装ファイルを区別します
1 つのモジュール仕様につき複数のクラスを定義しないようにします
モジュール仕様に implementation-private 宣言を入れないようにします
モジュールのインライン関数定義を個別のファイルに配置します
プログラムのサイズが重要な場合は、大規模モジュールを複数の翻訳単位に分割します
プラットフォーム依存性を分離します
ファイルが重複インクルードされないよう保護します
No_Inline
条件付きコンパイル・シンボルを使用して、インライン・コンパイルを排除します
ネストされたステートメントには、小さくて一貫性のあるインデント・スタイルを使用します
関数のパラメーターを関数名またはスコープ名からインデントします
標準の印刷用紙サイズに収まる最大限の行の長さを使用します
一貫性のある改行を使用します
C 形式ではなく、C++ 形式のコメントを使用します
ソース・コードとコメントの近接性を最大限にします
行末でコメントを使用しません
コメント・ヘッダーを使用しません
空のコメント行を使用してコメントの段落を区切ります
冗長性を排除します
コメントではなくコードだけ読んでも意味が分かるコードを記述します
クラスと関数を文書化します
命名規則を選択し、一貫して適用します
大文字と小文字の区別以外に違いのない型名は使用しないようにします
省略語を使用しないようにします
言語構造を表す目的で接尾辞を使用しないようにします
明確で、読みやすく、意味のある名前を選択します
名前には正しいスペルを使用します
論理型に対して肯定的な述部の文節を使用します
名前空間を使用して潜在的なグローバル名をサブシステムまたはライブラリー別に分割します
クラス名には名詞または名詞句を使用します
手続き型の関数名には動詞を使用します
同じ一般的意味を意図するときは関数の多重定義を使用します
文法要素で名前を補って意味を強調します
否定的な意味を持つ例外名を選択します
例外名にプロジェクトで定義した形容詞を使用します
浮動小数点数の指数と 16 進数の数字には大文字を使用します
名前空間を使用してクラス以外の機能性をグループ化します
グローバル/名前空間スコープのデータをできるだけ使用しないようにします
抽象データ型の実装に struct ではなく class を使用します
アクセス可能度が低くなる順にクラス・メンバーを宣言します
抽象データ型に対して public または protected のデータ・メンバーを宣言しません
friend を使用してカプセル化を維持します
クラス宣言の中で関数を定義しません
変換演算子と単独のパラメーター・コンストラクターの宣言が多すぎないようにします
virtual でない関数は慎重に使用します
コンストラクター内での代入ではなくコンストラクター初期化子を使用します
コンストラクター、デストラクターからのメンバー関数の呼び出しは慎重に行います
整数のクラス定数に対して static const を使用します
常に明示的な関数戻り型を宣言します
関数宣言では常に仮パラメーター名を与えます
関数の戻りポイントは可能なかぎり 1 つだけにします
グローバルな副次作用を伴う関数を作成しないようにします
重要度と変動性が低くなる順に関数パラメーターを宣言します
パラメーターの個数が可変の関数を宣言しないようにします
デフォルト・パラメーターを持つ関数を再宣言しないようにします
関数宣言内で const を最大限使用します
値によってオブジェクトを渡さないようにします
マクロ拡張用の #define
ではなくインライン関数を使用します
関数の多重定義ではなくデフォルト・パラメーターを使用します
関数の多重定義を使用して共通のセマンティクスを表現します
ポインターと整数を引数に取る関数を多重定義しないようにします
複雑さを最小限にします
基本型を使用しません
リテラル値を使用しません
定数の定義にプリプロセッサーの #define
指令を使用しません
最初の使用箇所の近くでオブジェクトを宣言します
const オブジェクトを常に宣言時に初期化します
オブジェクトを定義時に初期化します
真偽値式で分岐するときは if ステートメントを使用します
不連続な値で分岐するときは switch ステートメントを使用します
ループで反復前テストが必要なときは、for ステートメントまたは while ステートメントを使用します
ループで反復後テストが必要なときは、do-while ステートメントを使用します
ループでは jump ステートメントの使用を避けます
ネストされたスコープの中に ID を隠蔽しないようにします
開発中はアサーションを多用してエラーを検出します
本当に例外的な状況に対してだけ例外を使用します
標準の例外からプロジェクト用の例外を派生させます
特定の抽象レベルで使用する例外の数を最小限にします
送出されるすべての例外を宣言します
最も派生度の高いクラスから最も基本的なクラスの順序で例外ハンドラーを定義します
catch-all 例外ハンドラーを使用しません
関数のステータス・コードが適切な値になるようにします
安全性のチェックはローカルで行うようにし、クライアントが行うようにしてはいけません
可能なかぎり「汎用的な」定数を使用します
可能なかぎり標準のライブラリー・コンポーネントを使用します
プロジェクト全体のグローバル・システム型を定義します
typedef を使用して同義語を作成しローカルな意味を強化します
括弧を多く使用して、複合的な式を分かりやすくします
式のネストが深くなりすぎないようにします
NULL ポインターとしては、NULL
ではなく 0 を使用するようにします
例外が最初に発生した時点で通知します