許可されている言語機能

これらの規則は厳密なものではありませんが、これらから逸脱する場合は、非常に明確な理由が必要です。

移植に関する考慮事項

ほとんどの|Gromacs|ファイルはC++17でコンパイルされますが、一部のファイルはC99でコンパイルされます。C++には多くの機能がありますが、|Gromacs|のコードを保守しやすく、読みやすくするため、一部の機能は使用しません。基本的な考え方は、できるだけシンプルにすることです。

  • MSVC は C99 の一部のみをサポートしており、その場合は回避策が必要です。

  • 私たちがほぼすべての C++17 の機能を活用できるはずです。例外については、「GPU API に関する考慮事項」を参照してください。

C++ 標準ライブラリ

GROMACS コードは、サポートされているプラットフォームで利用可能な C++17 標準ライブラリの最も基本的な機能に対応する必要があります。 現代的な機能の中には、移植する価値があるほど有用なものもあります。 一貫性があり、将来互換性のあるヘッダーは、src/gromacs/compat/ に、Library documentation で説明されているように提供されています。

一般的な考慮事項

基本として、GROMACS は C++ Core Guidelines c++ guidelines に準拠しますが、このプロジェクト独自のより具体的なガイドラインがある場合は、それらに従います。当社は、さまざまな C++ コンパイラでコードがコンパイルされる必要があるため、また可読性を向上させるために、いくつかの点でより厳格な方針をとる傾向があります。ただし、GROMACS は、常に開発されている高度なプロジェクトであり、当社のニーズが進化するにつれて、これらの点について、緩めたり、より厳格にしたりすることがあります。これらの変更は、コードレビューにおける合意の一部として自然に発生する場合があります。また、意見が一致しない主要な部分は、issue tracker のスレッドで議論する必要があります。大規模な変更は、各リリースにおける開発の初期段階で提案することで、リリース直前の最後の瞬間になって、コンパイラのエラーに遭遇するのを防ぐことができます。

  • 名前空間を使用する。

    • GROMACS、gmxapinblib``ライブラリ/レイヤー内のすべての要素は、それぞれ ``gmxgmxapi``nblib``の命名空間内に配置する必要があります。詳細は|linkref10|を参照。

    • テストで使用するコードは、明確にテスト対象のコードと、GoogleTestで使用される「testing」という名前空間から区別できるように、ネストされた test という名前空間内に配置する必要があります。

    • ソースファイルで匿名名前空間を使用し、外部リンクを持たないべきシンボルを記述します(|linkref12|を参照)。

    • ヘッダーファイルで internal 名前空間を使用し、依存できない実装の詳細を示す。これは、匿名名前空間を使用できないため(here を参照)です。

    • それ以外の場合、モジュールヘッダーで関連するフリー関数群を公開する必要がある場合にのみ、ネストされた名前空間の使用は避けてください。

    • ヘッダーには using を使用しないようにし、一般的な名前のエイリアスとしてのみ使用し、ファイルレベルでの using namespace gmx などの使用は避けてください。 まだ更新されていないファイルで、わずかな数の gmx 名前空間シンボルのみが必要な場合は、それらのシンボルのみをインポートすることを検討してください。 here を参照してください。

    • 通常、「gmx::」プレフィックスの使用は、その名前空間内で避けるべきです。ただし、明確さや正確性が重要な場合は、使用してください。例えば、異なる名前空間で同じ名前を区別する場合に使用します。

    • 基本データ型(例:int64_tsize_t)を除き、通常はSTLのシンボルには`std::`プレフィックスを使用します。

  • STLを使用しますが、ユニットテスト以外では`iostreams`の使用は避けてください。`iostreams`は、使用状況によっては、他の文字列ストリーム形式と比較してパフォーマンスに悪影響を与える可能性があります。また、現在のコードで広く使用されているCの`stdio`ルーチンと同時に使用する場合、問題が発生する可能性があります。ただし、Googleのテストでは`iostreams`を使用しているため、ユニットテストコードでは使用してください。

  • 関数パラメータとして、非定数参照を使用しないでください。そうすると、関数呼び出しによってパラメータとして渡された変数が変更される可能性があるかどうかを、プロトタイプを確認せずに判断できなくなります。

  • 可能な限り not_null<T> ポインタを使用し、有効なポインタへの参照が必要であり、参照は不適切であることを示すセマンティクスを伝えます。 herehere も参照してください。

  • string_view を使用して、const std::string & ではなく、単に読み取り専用の文字シーケンスのみを使用したい場合に役立ちます。詳細は、here を参照してください。ただし、一部の C API (例: fopen, fputs, fprintf) が期待する null 終端が保証されないため、string_view はこのような場合には使用しないでください。

  • ArrayRef<T> または ArrayRef<const T> (およびそれらのパディングされた形式) を使用して、連続する範囲(それぞれ、変更可能または変更不可能な)の値に対するビューを表現します。特に、関数パラメータとして使用します。この型はコピーが安価であり、イテレータパラメータやポインタとサイズパラメータのペアよりも表現力が豊かで(ただし、ArrayRef<T> に直接 restrict 属性を使用することはできません)。また、呼び出し元が、C配列、arrayvector``(任意の割り当てタイプ)、またはその他のものから簡単に値を提供できるようにすることで、柔軟性を提供します。これにより、実装側が複数のビューパラメータを持つ関数に対してオーバーロードのファミリーを提供する必要がなくなります。例えば、関数定義で ``const ArrayRef<T> を使用して、関数の実装がビューを変更しないことを明示的に示すことができます。ただし、ビュー内の値は変更できます。また、here および here などの C++ Core Guidelines の他の部分も参照してください。GROMACS は、この目的のために、C++20 の類似の span を採用します。

  • optional<T> タイプの型を使用し、T 型の値が存在しない理由が明確で、すべての関係者にとって理解しやすい場合に、また、T 型の値が存在しないことが、通常の T 型の値が存在する場合と同じくらい自然な場合に、使用してください。here の例として、文字列から整数値を解析する関数の戻り値、範囲内の一致する要素の検索、または残りの型に対するオプションの名前の提供などが挙げられます。リソースの遅延読み込み(デフォルトコンストラクタを持たないオブジェクトなど)には、optional<T> を使用しないでください。ただし、T 型の値が存在しない理由を説明する必要がある場合は、他の構文を使用してください。例えば、エラー処理には optional<T> を使用しないでください。optional<T> は「ポインタではなくオブジェクトをモデル化し、operator*() および operator->() が定義されている」(cppreference) ことを意味します。動的なメモリ割り当ては発生せず、optional<T> に格納されているオブジェクトへの前置宣言は機能しません。したがって、ハンドルを渡す場合は、optional<T> の使用は避けてください。unique_ptr と異なり、optional<T> は値のセマンティクスを持ち、参照のセマンティクスを持ちません。

  • Cスタイルのキャストを使用しないでください。適切な場合は、const_caststatic_cast、または reinterpret_cast``を使用してください。 ``dynamic_cast``については、RTTIに関するセクションを参照してください。 型を強調表示するために(例えば、意図的な整数除算の場合)、コンストラクタ構文を使用します。 実際の定数を作成するには、ユーザー定義の文字列表 ``_real``を使用します(例えば、``2.5_real``ではなく``static_cast<real>(2.5))。

  • 算術演算(ループインデックスを含む)には、符号付き整数を使用します。 ssize``(フリー関数および ``ArrayRef のメンバーとして利用可能)を使用することで、キャストを回避できます。

  • 関数を過剰に使用しないようにしてください。関数が本当に同じことを異なる型で実行する場合を除き、代わりに関数名をより記述的にしてください。

  • デフォルトの関数引数の使用は避けてください。これによって、コードがより読みにくくなる可能性があります(|linkref3|を参照)。ただし、特定のケースで可読性が向上すると判断した場合は、その使用を正当化することができます(|linkref4|を参照)。

  • 過度な演算子の使用は、本当にそれが最適な方法であるかどうかを十分に検討してから行ってください。 &&||、またはカンマ演算子の過度な使用は避けてください。なぜなら、これらの演算子の評価順序を維持することは不可能だからです。

  • できる限り、複雑なテンプレート、複雑なテンプレートの特殊化、または SFINAE などのテクニックの使用を避けるようにしてください。少なくとも、これらはコードの理解を難しくする可能性があります。

  • 複数の継承は使用しないでください。複数の純粋なインターフェースから継承することは可能ですが、その場合、少なくとも1つのベースクラス(通常は最初のベースクラス)にのみコードが含まれている必要があります。詳細については、|linkref5|と|linkref6|を参照してください。

  • 過度に複雑な継承グラフを作成しないでください。わずかなコード削減のために、実装を継承するのを避け、代わりに「再利用のために継承する、再利用のために継承しない」という原則に従ってください。また、実装とインターフェースの継承を混在させるべきではありません。詳細については、|linkref7|を参照してください。

  • 不要なヘッダーを含めないでください。ヘッダーファイルでは、ヘッダーファイル内で「名前」のみで指定されている型を使用する場合、その型の名前を前述する方が推奨されます。これにより、コンパイル時の依存関係が減り、結果として時間が短縮されます。また、ソースファイルが型を名前のみで利用する場合(例えば、呼び出し元から受け取ったポインタを呼び出し元に渡す場合)、インクルードステートメントは不要です!

  • 積極的にアサーションを活用して、意図を明確に記述してください(ただし、アサーションが不要になるようにコードを記述することを優先してください)。

  • GMX_ASSERT()GMX_RELEASE_ASSERT() を、生の assert() よりも優先して使用してください。なぜなら、これらの関数を使用すると、説明的なテキストを追加できます。

  • gmx::Mutex を使用して、pthreads、std、または生のスレッド/MPI ミューテックスを使用する代わりに。

  • 変数の型が限られた値のいずれか1つのみを許可する場合、適切なenumを使用してください。C++は、このようなコードでのエラーを検出するのにCよりも優れています。理想的には、すべてのenumはtyped enumである必要があります。詳細は、|linkref8|を参照してください。

  • 新しいクラスを作成する際には、そのクラスのコピーが必要かどうかを検討してください。もし不要であれば、コピーコンストラクタと代入演算子をプライベートに宣言し、定義しないことで、そのクラスのオブジェクトのコピーを試みることを無効にすることができます。コピーを許可する場合は、コピーコンストラクタと代入演算子を提供するか、コンパイラが生成したものを利用することを示唆する明確なコメントを記述し(そして、それらが期待どおりに動作することを確認する)、src/gromacs/utility/classhelpers.h には、この操作を効率的に行うための便利なマクロがいくつか用意されています。この場合、削除された関数も利用できます。

  • すべての1つのパラメータを持つコンストラクタを明示的に宣言することは、本当に理解している場合にのみ行ってください。そうでない場合、それらは暗黙的な型変換に使用され、コードを理解しにくくしたり、コンパイラが報告するはずのバグを隠したりする可能性があります。同様の理由から、クラスを他の型に変換するための演算子を宣言する前に、十分に検討してください。詳細については、|linkref9|を参照してください。

  • const 修飾されたコードを記述してください(const_cast は、どうしても必要な場合にのみ使用してください)。

  • 本当に必要な場合にのみ、RTTI(実行時型情報、実際には dynamic_casttypeid)の使用を避けてください。RTTIの使用には、バイナリサイズ(コンパイル時に常に発生)と実行時間(使用した場合のみ)の両方で、非常に高いコストがかかります。問題がRTTIを必要とするように見える場合でも、代替設計がないかどうかを検討してください。多くの場合、そのような代替設計の方が優れています。

  • コンパイラのメタデータ伝播に依存しないでください。構造体の要素やキャプチャされたラムダ関数のパラメータは、コンパイラによって通常「restrict」やアライメント修飾子が削除されるため、その構造体のインスタンスを定義したり、またはそのインスタンスを保持するためのメモリを割り当てたりした後、そのデータメンバーがアライメントされていない可能性があります。

  • 計算リソースに最適化されたカーネルで実行されるコードにおいて、再利用に適したデータレイアウトと、SIMDメモリ操作のための適切なアライメントを確保する計画

  • 認識することが重要です。コードのいくつかの部分は異なる要件を持っています。具体的には、カーネルの計算、mdrun の設定コード、高レベルの MD ループコード、シミュレーション設定ツール、および分析ツールはそれぞれ異なるニーズがあり、正しさとレビュー時間、開発者時間、コンパイル時間、実行時間とのトレードオフは異なります。

  • auto を変数定義に使用する際は、慎重に検討してください。変数の型が将来のコードを読む人にとってすぐに明らかである場合、または完全に不要な場合、auto の使用は問題ありません。ただし、特定のケースでは、例えばジェネリックなテンプレートと組み合わせて auto を使用する必要がある場合があります。特に、イテレータやラムダなどの長い型の場合、明示的に型を指定すると可読性が低下する可能性があるため、auto の使用を推奨します。もし不明な場合は、auto の使用は避けるようにしてください。

  • クラスを作成する際には、まず、コンストラクタ、デストラクタ、移動およびコピー操作などの共通のインフラストラクチャを記述し、次に、パブリックなメンバー関数、次にプライベートなメンバー関数、最後にプライベートなデータを記述します。 新しく作成されたクラスには、パブリックなデータは含まれていません。

  • 「共通の「定数」インフラオブジェクト(例:MpiCommgmx_wallcycle)へのハンドル(つまり、ポインタ、参照)の保存を優先します。これらのオブジェクトは小さく、コピーも高速であるためです。モジュールが一貫した使用パターンを持ち、デバッグが容易であり、(可能な限り)、型名のみに依存するようにしたいと考えています。」

エラー処理のための例外の実装

詳細については、エラー処理 のセクションを参照してください。これは、実行時のエラーを処理する方法(例:例外を使用する方法)について説明しています。

  • 例外安全なコードを記述する。すべての新しいコードは、これを実現するために、少なくとも基本的な例外安全性または例外をスローしないという保証を提供する必要がある。

  • 可能な限り、std (またはカスタム) コンテナを使用してください。

  • メモリ管理にはスマートポインタを使用してください。 デフォルトでは、std::unique_ptrgmx::unique_cptr を、必要な new または snew の呼び出しと組み合わせて使用します。 std::shared_ptr は、ライフタイムの管理を共有する必要がある場合に利用できます。 malloc は絶対に使用しないでください。

  • リソース(メモリ、ミューテックス、ファイルハンドルなど)の管理には、RAII(Resource Acquisition Is Initialization)を使用する。

  • 古い、例外を扱えない関数から、例外をスローする可能性のある関数を呼び出すことは避けることが望ましいです。ただし、std::vectorstd::string などの、メモリ不足時に std::bad_alloc をスローする可能性のある機能を使用するために、この制限を緩和します。特に、GROMACS は、多くの古い C スタイルのメモリ管理を使用しており、ツールが機能を追加するにつれて、チェックツールは引き続き有効な警告を発しています。これらの問題を古い構文で修正することは、開発者の時間を無駄にするためです。

  • 関数/メソッドは、例外安全かどうか、および例外(間接的にでも)を発生させる可能性があるかどうか、そして発生させる可能性がある場合、どのような例外を発生させる可能性があるかをコメントで明示する必要があります。

GPU API に関する考慮事項

  • OpenCL を C (特に C99) 言語として記述します。OpenCL カーネルで C++ を使用することは推奨されていません。

  • SYCL 2020規格を使用してください。パフォーマンス向上のために、ベンダー固有の拡張機能やバックエンド固有のコードを使用できますが、他のすべてのサポートされているターゲットに対して、適切な代替手段を提供する必要があります。

  • SYCLコードで``sycl::buffer``の代わりに、USMと順序付きキューを使用してください。これにより、コードの一貫性がGPUバックエンド全体で向上します。さらに、バッファはカーネルでのコンパイラによる最適化が難しいため、パフォーマンスが低下する可能性があります(2022年時点)。

前処理に関する考慮事項

  • 直接、ビルド構成に関連するものではありません。プレプロセッサの定義を使用するのではなく、テンプレートやインライン関数を使用してコードを生成し、定数には、列挙型または定数変数を活用してください。

  • ビルド構成に使用されるプレプロセッサ変数は、常に有効な値が定義されているように整理する必要があります。つまり、プレプロセッサ変数が定義されているかどうかを確認するのではなく、その値を常に確認します。これにより、保守性が向上します。コンパイラは、変数が定義されていないことを通知できます。

  • 長いコードセクションを避け、コンパイル時に #if (またはそれよりも悪い、外部から提供されるシンボルを使用した #ifdef) に依存しないコードを書くようにしてください。

  • ソースコードファイルの先頭で定数変数の定義を記述し、その変数をコード内で使用することを推奨します。これにより、すべてのコンパイルパスがすべての構成で構築されるため、潜在的なバグの発生を減らすことができます。

  • ネストされたプリプロセッサ条件を、ネストが必要で、インデントしない場合に比べて結果がより明確になる場合は、インデントしてください。

  • 長期間にわたるコード領域が必要な場合、およびその利点がある場合は、プレプロセッサの条件を領域の最後に繰り返すコメントを検討することを強く推奨します。長期間のコード領域を理解し、デバッグするのに非常に役立ちます。