LLVMコーディング規約¶
はじめに¶
このドキュメントでは、LLVMプロジェクトで使用されているコーディング規約について説明します。すべての状況で守るべき絶対的な要件としてコーディング規約をみなすべきではありませんが、ライブラリベースの設計(LLVMのような)に従う大規模なコードベースでは特に重要です。
このドキュメントでは、機械的なフォーマットの問題、空白、その他の「微細な詳細」に関するガイダンスを提供する場合がありますが、これらは固定された標準ではありません。常に黄金律に従ってください。
既に実装されているコードを拡張、拡張、またはバグ修正する場合は、ソースが統一され、追跡しやすくなるように、既に使用されているスタイルを使用してください。
一部のコードベース(例:libc++
)では、コーディング規約から逸脱する特別な理由があります。たとえば、libc++
の場合、これは命名およびその他の規則がC++標準によって規定されているためです。
コードベースでは一貫して守られていない規則もあります(例:命名規則)。これは、これらの規則が比較的新しいものであり、多くのコードが導入される前に記述されていたためです。私たちの長期的な目標は、コードベース全体が規則に従うことですが、既存のコードの大規模なリフォーマットを行うパッチは明確に**望んでいません**。一方、別の方法で変更しようとしている場合にクラスのメソッド名を変更することは妥当です。コードレビューを容易にするために、このような変更は別々にコミットしてください。
これらのガイドラインの究極の目標は、共通ソースベースの可読性と保守性を向上させることです。
言語、ライブラリ、および標準¶
LLVMおよびこれらのコーディング規約を使用するその他のLLVMプロジェクトのほとんどのソースコードは、C++コードです。環境の制限、歴史的な制限、またはツリーにインポートされたサードパーティのソースコードのために、Cコードが使用されている場所もあります。一般的に、標準に準拠した、現代的で移植可能なC++コードを実装言語として選択することを優先しています。
自動化、ビルドシステム、ユーティリティスクリプトのために、Pythonが推奨されており、LLVMリポジトリですでに広く使用されています。
C++標準バージョン¶
特に明記されていない限り、LLVMサブプロジェクトは標準C++17コードを使用して記述されており、不要なベンダー固有の拡張機能は使用していません。
それにもかかわらず、ホストコンパイラとしてサポートされている主要なツールチェーンで使用可能な機能に限定しています(LLVMシステムの使用方法ページの「ソフトウェア」セクションを参照)。
各ツールチェーンは、それが受け入れるものに関する優れたリファレンスを提供します。
さらに、cppreference.comには、サポートされているC++機能のコンパイラ比較表があります。
C++標準ライブラリ¶
カスタムデータ構造を実装する代わりに、特定のタスクで使用可能な場合は、C++標準ライブラリ機能またはLLVMサポートライブラリを使用することを推奨します。LLVMおよび関連プロジェクトは、標準ライブラリ機能とLLVMサポートライブラリをできる限り重視し、依存しています。
LLVMサポートライブラリ(たとえば、ADT)は、標準ライブラリにない特殊なデータ構造または機能を実装します。このようなライブラリは通常llvm
名前空間で実装され、標準インターフェースがある場合は、そのインターフェースに従います。
C++とLLVMサポートライブラリの両方が同様の機能を提供し、C++実装を優先する特定の理由がない場合、一般的にLLVMライブラリを使用することを優先します。たとえば、llvm::DenseMap
はほとんど常にstd::map
またはstd::unordered_map
の代わりに使用し、llvm::SmallVector
は通常std::vector
の代わりに使用します。
I/Oストリームなど、一部の標準機能は明示的に回避し、代わりにLLVMのストリームライブラリ(raw_ostream)を使用します。これらのトピックに関するより詳細な情報は、LLVMプログラマーズマニュアルで入手できます。
LLVMのデータ構造とそのトレードオフの詳細については、プログラマーズマニュアルの該当セクションを参照してください。
Pythonバージョンとソースコードのフォーマット¶
必要なPythonの現在の最小バージョンは、LLVMシステムの使用方法セクションに記載されています。LLVMリポジトリ内のPythonコードは、このバージョンのPythonで使用可能な言語機能のみを使用する必要があります。
LLVMリポジトリ内のPythonコードは、PEP 8に記載されているフォーマットガイドラインに従う必要があります。
一貫性を保ち、変更を制限するために、コードはPEP 8に準拠したblackユーティリティを使用して自動的にフォーマットする必要があります。デフォルトのルールを使用してください。--line-length
はデフォルトで80ではないため、指定しないでください。デフォルトのルールは、blackのメジャーバージョン間で変更される可能性があります。フォーマットルールの不要な変更を回避するために、現在LLVMではblackバージョン23.xを使用しています。
フォーマットに関連しないパッチを貢献する場合、パッチで変更されたPythonコードのみをフォーマットする必要があります。この目的のために、変更されたPythonコードに対してのみデフォルトのblackルールを実行するdarkerユーティリティを使用します。これにより、LLVMのpre-commit CI(darkerも使用)でのPythonフォーマットチェックにパッチが合格することが保証されます。Pythonファイルのフォーマットを目的としたパッチを貢献する場合は、blackを使用します。blackは現在、ファイル全体のフォーマットのみをサポートしています。
いくつかの簡単な例を以下に示しますが、詳細についてはblackとdarkerのドキュメントを参照してください。
$ pip install black=='23.*' darker # install black 23.x and darker
$ darker test.py # format uncommitted changes
$ darker -r HEAD^ test.py # also format changes from last commit
$ black test.py # format entire file
個々のファイル名ではなく、ディレクトリをdarkerに指定でき、変更されたファイルが検出されます。ただし、LLVMリポジトリのクローンなど、ディレクトリが大きい場合、darkerは非常に遅くなる可能性があります。その場合は、変更されたファイルをリストするためにgitを使用することをお勧めします。たとえば、
$ darker -r HEAD^ $(git diff --name-only --diff-filter=d HEAD^)
機械的なソースの問題¶
ソースコードのフォーマット¶
コメント¶
コメントは、可読性と保守性を高めるために重要です。コメントを書く際は、適切な大文字化、句読点などを用いて、英語の文章として記述してください。コードが何をしようとしているか、そしてなぜそうしようとしているかを記述することを目指し、ミクロレベルでの方法は記述しないでください。文書化すべき重要な事項をいくつか紹介します。
ファイルヘッダー¶
すべてのソースファイルには、ファイルの基本的な目的を説明するヘッダーが必要です。標準的なヘッダーは次のようになります。
//===-- llvm/Instruction.h - Instruction class definition -------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.dokyumento.jp/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file contains the declaration of the Instruction class, which is the
/// base class for all of the VM instructions.
///
//===----------------------------------------------------------------------===//
この特定の形式について注意すべき点がいくつかあります。「-*- C++ -*-
」という文字列は、最初の行にあり、ソースファイルがC++ファイルではなくCファイルであることをEmacsに伝えるためです(Emacsはデフォルトで.h
ファイルをCファイルとみなします)。
注記
このタグは.cpp
ファイルでは必要ありません。ファイル名は最初の行に、ファイルの目的に関する非常に短い説明とともに記載されています。
ファイルの次のセクションは、ファイルがリリースされているライセンスを定義する簡潔な注記です。これにより、ソースコードを配布できる条件が明確になり、変更してはならないことを示します。
本文はDoxygenコメントです(通常の//
ではなく///
コメントマーカーで識別されます)。ファイルの目的を記述しています。最初の文(または\brief
で始まる文章)は概要として使用されます。追加情報は空行で区切る必要があります。アルゴリズムが論文に基づいている場合、または別のソースで説明されている場合は、参考文献を挙げてください。
ヘッダーガード¶
ヘッダーファイルのガードは、このヘッダーを使用するユーザーが#include
する際に使用する、すべて大文字のパスでなければなりません。パスセパレータと拡張子マーカーの代わりに「_」を使用します。たとえば、ヘッダーファイルllvm/include/llvm/Analysis/Utils/Local.h
は#include "llvm/Analysis/Utils/Local.h"
として#include
されるため、そのガードはLLVM_ANALYSIS_UTILS_LOCAL_H
です。
クラスの概要¶
クラスはオブジェクト指向設計の基本的な要素です。そのため、クラス定義には、クラスの用途と動作を説明するコメントブロックが必要です。すべての複雑なクラスには、doxygen
コメントブロックが必要です。
メソッドの情報¶
メソッドとグローバル関数も文書化する必要があります。それが何をするのかについての簡単なメモと、エッジケースの説明があれば十分です。読者はコード自体を読まずにインターフェースの使用方法を理解できる必要があります。
ここで説明すべき良い点は、予期せぬことが起こった場合に何が起こるかです。たとえば、メソッドはnullを返すでしょうか?
ドキュメントコメントでのDoxygenの使用¶
\file
コマンドを使用して、標準的なファイルヘッダーをファイルレベルのコメントに変換します。
すべての公開インターフェース(公開クラス、メンバ関数、非メンバ関数)について、説明的な段落を含めます。API名から推測できる情報を繰り返さないでください。最初の文(または\brief
で始まる段落)は概要として使用されます。\brief
は視覚的なごちゃごちゃを増やすため、1文を使用するようにしてください。詳細な説明は別の段落に入れてください。
段落内でパラメータ名を参照するには、\p name
コマンドを使用します。\arg name
コマンドは使用しないでください。これは、パラメータのドキュメントを含む新しい段落を開始するためです。
インラインではないコード例は、\code ... \endcode
で囲みます。
関数パラメータを文書化するには、\param name
コマンドで新しい段落を開始します。パラメータが出力パラメータまたは入出力パラメータとして使用される場合は、それぞれ\param [out] name
または\param [in,out] name
コマンドを使用します。
関数の戻り値を説明するには、\returns
コマンドで新しい段落を開始します。
最小限のドキュメントコメント
/// Sets the xyzzy property to \p Baz.
void setXyzzy(bool Baz);
Doxygenのすべての機能を推奨される方法で使用したドキュメントコメント
/// Does foo and bar.
///
/// Does not do foo the usual way if \p Baz is true.
///
/// Typical usage:
/// \code
/// fooBar(false, "quux", Res);
/// \endcode
///
/// \param Quux kind of foo to do.
/// \param [out] Result filled with bar sequence on foo success.
///
/// \returns true on success.
bool fooBar(bool Baz, StringRef Quux, std::vector<int> &Result);
ヘッダーファイルと実装ファイルでドキュメントコメントを複製しないでください。公開APIのドキュメントコメントはヘッダーファイルに記述します。非公開APIのドキュメントコメントは実装ファイルに記述できます。いずれの場合も、実装ファイルには、必要に応じて実装の詳細を説明する追加コメント(Doxygenマークアップではないものも可)を含めることができます。
コメントの先頭に関数名またはクラス名を複製しないでください。人間にとって、どの関数またはクラスが文書化されているかは明らかです。自動ドキュメント処理ツールは、コメントを正しい宣言にバインドするのに十分なインテリジェンスを持っています。
避けるべき例
// Example.h:
// example - Does something important.
void example();
// Example.cpp:
// example - Does something important.
void example() { ... }
推奨される例
// Example.h:
/// Does something important.
void example();
// Example.cpp:
/// Builds a B-tree in order to do foo. See paper by...
void example() { ... }
エラーメッセージと警告メッセージ¶
明確な診断メッセージは、ユーザーが自分の入力における問題を特定して修正するのに役立つため重要です。簡潔で正確な英語の文章を使用し、何が間違っていたのかを理解するために必要なコンテキストをユーザーに提供してください。また、他のツールによって一般的に生成されるエラーメッセージスタイルと一致させるために、最初の文を小文字で始め、最後の文は、そうでなければピリオドで終わる場合でもピリオドなしで終わらせてください。「セミコロンを忘れましたか?」など、異なる句読点で終わる文は、そのままにしてください。
たとえば、これは良いエラーメッセージです。
error: file.o: section header 3 is corrupt. Size is 10 when it should be 20
これは、役に立つ情報が提供されておらず、間違ったスタイルが使用されているため、悪いメッセージです。
error: file.o: Corrupt section header.
他のコーディング標準と同様に、Clang Static Analyzerなどの個々のプロジェクトには、これに従わない既存のスタイルがある場合があります。プロジェクト全体で異なるフォーマットスキームが首尾一貫して使用されている場合は、そのスタイルを使用してください。それ以外の場合は、この標準はclang、clang-tidyなど、すべてのLLVMツールに適用されます。
ツールまたはプロジェクトに警告またはエラーを出力する既存の関数がない場合は、Support/WithColor.h
で提供されているエラーと警告ハンドラを使用して、stderrに直接出力するのではなく、適切なスタイルで出力されるようにしてください。
report_fatal_error
を使用する場合は、通常のエラーメッセージと同様にメッセージの標準に従ってください。アサーションメッセージとllvm_unreachable
呼び出しは、自動的にフォーマットされるため、必ずしもこれらのスタイルに従う必要はありません。そのため、これらのガイドラインは適切ではない場合があります。
#include
スタイル¶
ヘッダーファイルコメント(およびヘッダーファイルで作業している場合はインクルードガード)の直後に、ファイルに必要な最小限の#includeリストを記述する必要があります。これらの#include
は、次の順序で記述することをお勧めします。
メインモジュールヘッダー
ローカル/プライベートヘッダー
LLVMプロジェクト/サブプロジェクトヘッダー(
clang/...
、lldb/...
、llvm/...
など)システム
#include
各カテゴリは、完全パスで辞書順にソートする必要があります。
メインモジュールヘッダーファイルは、.h
ファイルで定義されたインターフェースを実装する.cpp
ファイルに適用されます。この#include
は、ファイルシステム上のどこに存在するかに関係なく、常に最初に含める必要があります。.cpp
ファイルでヘッダーファイルを最初に含めることで、ヘッダーファイルに、ヘッダーファイルでは明示的に#include
されていないが、含めるべき隠れた依存関係がないことを保証します。.cpp
ファイルで、実装するインターフェースがどこで定義されているかを示すドキュメントの一種でもあります。
LLVMプロジェクトおよびサブプロジェクトのヘッダファイルは、上記で説明した理由により、最も具体的なものから最も抽象的なものの順にグループ化してください。例えば、LLDBはclangとLLVMの両方に依存し、clangはLLVMに依存します。そのため、LLDBのソースファイルでは、`lldb
`ヘッダを最初にインクルードし、次に`clang
`ヘッダ、最後に`llvm
`ヘッダをインクルードする必要があります。これにより、(例えば)メインソースファイルまたはそれ以前のヘッダファイルへのインクルードによって、LLDBヘッダが誤って欠けているインクルードを拾ってしまう可能性を減らすことができます。clangも同様に、llvmヘッダをインクルードする前に、自身のヘッダをインクルードする必要があります。このルールは、すべてのLLVMサブプロジェクトに適用されます。
ソースコードの幅¶
コードは80カラム内に収まるように記述してください。
開発者が複数のファイルを並べて表示できるようにするためには、コードの幅に制限を設ける必要があります。幅の制限を選択する際には、ある程度恣意的ですが、標準的なものを選択するのが良いでしょう。例えば、80カラムではなく90カラムを選択しても、大きな価値はなく、コードの印刷には不利です。また、多くのプロジェクトが80カラムを標準化しているため、既にエディタの設定を80カラムにしている人もいます(90カラムなど、他の設定ではなく)。
空白¶
ソースファイルでは、常にスペースをタブよりも優先してください。インデントレベルやインデントスタイルには個人の好みがあり、それは問題ありません。問題なのは、エディタやビューアによってタブの展開幅が異なることです。これにより、コードが全く読めなくなる可能性があり、対処する価値はありません。
常に、上記の黄金律に従ってください。コードを修正または拡張する場合は、既存のコードのスタイルに従ってください。
末尾の空白を追加しないでください。一般的なエディタの中には、ファイルを保存する際に末尾の空白を自動的に削除するものがあり、これにより、diffやコミットに無関係な変更が表示される原因となります。
ラムダ式をコードブロックのようにフォーマットする¶
複数行のラムダ式をフォーマットする際は、コードブロックのようにフォーマットします。ステートメントに複数行のラムダ式が1つだけで、そのステートメントにはラムダ式の後にある式がない場合は、コードブロックの標準的な2スペースインデントにインデントを下げます。これは、ステートメントの前の部分によってifブロックが開かれているかのように扱います。
std::sort(foo.begin(), foo.end(), [&](Foo a, Foo b) -> bool {
if (a.blah < b.blah)
return true;
if (a.baz < b.baz)
return true;
return a.bam < b.bam;
});
このフォーマットを最大限に活用するには、継続または単一の呼び出し可能な引数(関数オブジェクトまたは`std::function
`)を受け付けるAPIを設計する場合は、可能な限り最後の引数にする必要があります。
ステートメントに複数行のラムダ式が複数ある場合、またはラムダ式の後に追加のパラメータがある場合は、`[]
`のインデントから2スペースインデントします。
dyn_switch(V->stripPointerCasts(),
[] (PHINode *PN) {
// process phis...
},
[] (SelectInst *SI) {
// process selects...
},
[] (LoadInst *LI) {
// process loads...
},
[] (AllocaInst *AI) {
// process allocas...
});
中括弧を使った初期化リスト¶
C++11以降、集約体の初期化に中括弧を使ったリストが大幅に増加しました。例えば、式の中で集約体のテンポラリオブジェクトを構築するために使用できます。現在では、ローカル変数から集約体(オプション構造体など)を構築するために、互いにネストしたり、関数呼び出しの中にネストしたりすることが自然な方法になっています。
集約変数の初期化の中括弧を使った従来の一般的なフォーマットは、深いネスト、一般的な式のコンテキスト、関数引数、ラムダ式とはうまく混ざりません。新しいコードでは、中括弧を使った初期化リストのフォーマットに、単純なルールを使用することを提案します。中括弧を関数呼び出しの括弧のように扱うことです。フォーマットルールは、ネストされた関数呼び出しのフォーマットについて既に広く理解されているものと完全に一致しています。例:
foo({a, b, c}, {1, 2, 3});
llvm::Constant *Mask[] = {
llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 0),
llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 1),
llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 2)};
このフォーマットスキームは、Clang Formatなどのツールで予測可能で一貫性があり、自動的なフォーマットを容易にするためにも特に役立ちます。
言語とコンパイラの問題¶
コンパイラの警告をエラーとして扱う¶
コンパイラの警告は多くの場合役に立ち、コードの改善に役立ちます。役に立たない警告は、小さなコード変更で抑制することがよくあります。例えば、if
条件での代入は、多くの場合タイプミスです。
if (V = getValue()) {
...
}
上記のコードに対しては、いくつかのコンパイラが警告を出力します。これは括弧を追加することで抑制できます。
if ((V = getValue())) {
...
}
移植可能なコードを書く¶
ほとんどの場合、完全に移植可能なコードを書くことが可能です。移植不可能なコードに依存する必要がある場合は、明確に定義され、適切に文書化されたインターフェースの背後に配置してください。
RTTIや例外を使用しない¶
コードサイズと実行ファイルサイズを削減するために、LLVMは例外やRTTI(ランタイム型情報、例えば`dynamic_cast<>
`)を使用しません。
ただし、LLVMは`isa<>, cast<>, and dyn_cast<>`のようなテンプレートを使用する、独自に作成されたRTTIを広く使用しています。この形式のRTTIはオプトインであり、任意のクラスに追加できます。
C++スタイルのキャストを優先する¶
キャストを行う場合は、Cスタイルのキャストではなく、`static_cast
`、`reinterpret_cast
`、`const_cast
`を使用してください。これには2つの例外があります。
使用されていない変数に関する警告を抑制するために`
void
`にキャストする場合(`[[maybe_unused]]
`の代わりに)。この場合は、Cスタイルのキャストを優先してください。整数型(厳密に型指定されていない列挙型を含む)間でキャストを行う場合、`
static_cast
`の代わりに関数スタイルのキャストが許可されます。
静的コンストラクタを使用しない¶
静的コンストラクタとデストラクタ(例えば、コンストラクタまたはデストラクタを持つ型のグローバル変数)は、コードベースに追加せず、可能な限り削除する必要があります。
異なるソースファイルにあるグローバル変数は、任意の順序で初期化されるため、コードの推論が困難になります。
静的コンストラクタは、LLVMをライブラリとして使用するプログラムの起動時間に悪影響を及ぼします。LLVMターゲットやその他のライブラリを追加でアプリケーションにリンクすることのコストをゼロにしたいと考えていますが、静的コンストラクタはその目標を損なっています。
class
キーワードとstruct
キーワードの使用¶
C++では、`class
`と`struct
`キーワードはほぼ互換性があります。違いはクラスを宣言する場合のみです。`class
`はすべてのメンバをデフォルトでprivateにするのに対し、`struct
`はすべてのメンバをデフォルトでpublicにします。
特定の`
class
`または`struct
`のすべての宣言と定義では、同じキーワードを使用する必要があります。例:
// Avoid if `Example` is defined as a struct.
class Example;
// OK.
struct Example;
struct Example { ... };
すべてのメンバがpublicとして宣言されている場合は、`
struct
`を使用する必要があります。
// Avoid using `struct` here, use `class` instead.
struct Foo {
private:
int Data;
public:
Foo() : Data(0) { }
int getData() const { return Data; }
void setData(int D) { Data = D; }
};
// OK to use `struct`: all members are public.
struct Bar {
int Data;
Bar() : Data(0) { }
};
中括弧を使った初期化リストでコンストラクタを呼び出さない¶
C++11以降、「一般化された初期化構文」により、中括弧を使った初期化リストを使用してコンストラクタを呼び出すことができます。複雑なロジックを持つコンストラクタを呼び出す場合、または特定のコンストラクタを呼び出していることを気にする場合は、これを使用しないでください。それらは、集約体の初期化のようにではなく、括弧を使った関数呼び出しのように見えるはずです。同様に、明示的に型を指定してコンストラクタを呼び出してテンポラリオブジェクトを作成する必要がある場合は、中括弧を使った初期化リストを使用しないでください。代わりに、集約体の初期化または概念的に同等の処理を行う場合は、(テンポラリオブジェクトの場合、型を指定せずに)中括弧を使った初期化リストを使用してください。例:
class Foo {
public:
// Construct a Foo by reading data from the disk in the whizbang format, ...
Foo(std::string filename);
// Construct a Foo by looking up the Nth element of some global data ...
Foo(int N);
// ...
};
// The Foo constructor call is reading a file, don't use braces to call it.
std::fill(foo.begin(), foo.end(), Foo("name"));
// The pair is being constructed like an aggregate, use braces.
bar_map.insert({my_key, my_value});
変数を初期化するときに中括弧を使った初期化リストを使用する場合は、左中括弧の前に等号を使用してください。
int data[] = {0, 1, 2, 3};
auto
型推論を使用してコードの可読性を向上させる¶
C++11では「ほぼ常に`auto
`を使用する」という方針を提唱する人もいますが、LLVMではより穏健な立場を取っています。コードの可読性や保守性を向上させる場合にのみ`auto
`を使用してください。「ほぼ常に」`auto
`を使用しないでください。ただし、`cast<Foo>(...)
`のような初期化子や、コンテキストから型が既に明らかなその他の場所では`auto
`を使用してください。`auto
`がこれらの目的に適しているもう1つの場合として、型が既に抽象化されている場合(多くの場合、`std::vector<T>::iterator
`などのコンテナのtypedefの背後にある場合)があります。
同様に、C++14では、パラメータ型が`auto
`になるジェネリックラムダ式が追加されました。テンプレートを使用する場合に使用してください。
auto
による不要なコピーに注意する¶
auto
の便利さゆえに、そのデフォルト動作がコピーであることを忘れがちです。特に範囲ベースの for
ループでは、不用意なコピーはコスト高となります。
コピーする必要がない限り、値には auto &
を、ポインタには auto *
を使用してください。
// Typically there's no reason to copy.
for (const auto &Val : Container) observe(Val);
for (auto &Val : Container) Val.change();
// Remove the reference if you really want a new copy.
for (auto Val : Container) { Val.change(); saveSomewhere(Val); }
// Copy pointers, but make it clear that they're pointers.
for (const auto *Ptr : Container) observe(*Ptr);
for (auto *Ptr : Container) Ptr->change();
ポインタの順序による非決定性の危険性¶
一般的に、ポインタには相対的な順序がありません。その結果、集合やマップなどの順序付けられていないコンテナをポインタキーで使用すると、反復順序は未定義になります。したがって、そのようなコンテナを反復処理すると、非決定的なコード生成が発生する可能性があります。生成されたコードは正しく動作するかもしれませんが、非決定性によってバグの再現やコンパイラのデバッグが困難になる可能性があります。
順序付けられた結果が期待される場合は、反復処理前に順序付けられていないコンテナをソートしてください。または、ポインタキーを反復処理する場合は、vector
/MapVector
/SetVector
などの順序付けられたコンテナを使用してください。
等しい要素のソート順序の非決定性の危険性¶
std::sort
は、等しい要素の順序が保持されないことが保証されない非安定ソートアルゴリズムを使用します。そのため、等しい要素を持つコンテナに std::sort
を使用すると、非決定的な動作が発生する可能性があります。このような非決定性のインスタンスを明らかにするために、LLVM は新しい llvm::sort ラッパー関数を導入しました。EXPENSIVE_CHECKS ビルドでは、これはソート前にコンテナをランダムにシャッフルします。std::sort
の代わりに llvm::sort
を使用するようにしてください。
スタイルの問題¶
上位レベルの問題¶
自己完結型ヘッダー¶
ヘッダーファイルは自己完結型(単独でコンパイル可能)で、.h
で終わる必要があります。インクルードを意図した非ヘッダーファイルは .inc
で終わらせ、控えめに使用してください。
すべてのヘッダーファイルは自己完結型である必要があります。ユーザーとリファクタリングツールは、ヘッダーをインクルードするために特別な条件を遵守する必要はありません。具体的には、ヘッダーにはヘッダーガードがあり、必要な他のすべてのヘッダーをインクルードする必要があります。
自己完結型ではないインクルードを意図したファイルはまれにあります。これらは通常、別のファイルの中間など、特殊な場所にインクルードされることを意図しています。ヘッダーガードを使用しない場合や、前提条件をインクルードしない場合があります。そのようなファイルには .inc
拡張子を使用します。控えめに使用し、可能であれば自己完結型ヘッダーを優先してください。
一般的に、ヘッダーは1つ以上の.cpp
ファイルで実装する必要があります。これらの.cpp
ファイルはそれぞれ、最初にインターフェースを定義するヘッダーをインクルードする必要があります。これにより、ヘッダーの依存関係がすべてヘッダー自体に正しく追加され、暗黙的にならないことが保証されます。翻訳単位では、システムヘッダーをユーザーヘッダーの後でインクルードする必要があります。
ライブラリの階層化¶
ヘッダーファイルのディレクトリ(例:include/llvm/Foo
)は、ライブラリ(Foo
)を定義します。1つのライブラリ(そのヘッダーと実装の両方)は、その依存関係にリストされているライブラリからのものだけを使用する必要があります。
この制約の一部は、従来のUnixリンカーによって強制できます(MacとWindowsのリンカー、およびlldは、この制約を強制しません)。Unixリンカーは、コマンドラインで指定されたライブラリを左から右に検索し、ライブラリを再訪することはありません。このようにして、ライブラリ間の循環依存関係は存在できません。
これは、すべてのライブラリ間の依存関係を完全に強制するものではなく、重要なことに、インライン関数によって作成されたヘッダーファイルの循環依存関係を強制するものではありません。 「これは正しく階層化されているか」という質問に答える良い方法は、すべてのインライン関数がインライン展開されなかった場合に、Unixリンカーがプログラムのリンクに成功するかどうかを検討することです。(および依存関係のすべての有効な順序について-リンク解決は線形であるため、いくつかの暗黙的な依存関係が紛れ込む可能性があります。AはBとCに依存するため、有効な順序は「CBA」または「BCA」です。どちらの場合も、明示的な依存関係はその使用の前に来ます。しかし、最初のケースでは、BはCに暗黙的に依存している場合でも、または2番目のケースではその逆でも、正常にリンクされる可能性があります)
#include
をできる限り少なくする¶
#include
はコンパイル時間の性能を低下させます。特にヘッダーファイルでは、必要がない限り行わないでください。
しかし待ってください!クラスの定義を、それを使用する場合、またはそれから継承する場合に必要とする場合があります。これらの場合は、そのヘッダーファイルを#include
してください。ただし、クラスの完全な定義を必要としない場合が多くあります。クラスへのポインタまたは参照を使用している場合、ヘッダーファイルは必要ありません。プロトタイプ化された関数またはメソッドからクラスインスタンスを返すだけの場合も必要ありません。実際、ほとんどの場合、クラスの定義は必要ありません。そして、#include
しないことでコンパイルが高速化されます。
ただし、この推奨事項をやり過ぎないように注意する必要があります。使用しているすべてのヘッダーファイルを**必ず**インクルードする必要があります—それらを直接、または別のヘッダーファイルを通じて間接的にインクルードできます。モジュールヘッダーにヘッダーファイルを誤ってインクルードし忘れないようにするには、実装ファイルで最初にモジュールヘッダーをインクルードしてください(上記参照)。これにより、後でわかる隠れた依存関係がなくなります。
「内部」ヘッダーをプライベートに保つ¶
多くのモジュールは複雑な実装を持っており、複数の実装(.cpp
)ファイルを使用します。内部通信インターフェース(ヘルパークラス、追加関数など)を公開モジュールヘッダーファイルに入れるのはよくあることです。しかし、それはしないでください!
本当にそのようなことをする必要がある場合は、ソースファイルと同じディレクトリにプライベートヘッダーファイルを入れ、ローカルでインクルードしてください。これにより、プライベートインターフェースがプライベートに保たれ、外部から邪魔されないことが保証されます。
注記
公開クラス自体に追加の実装メソッドを入れるのは問題ありません。プライベート(またはプロテクト)にするだけで問題ありません。
名前空間修飾子を使用して、以前に宣言された関数をインプリメントする¶
ソースファイルで関数のインライン展開ではない実装を提供する場合、ソースファイルで名前空間ブロックを開かないでください。代わりに、名前空間修飾子を使用して、定義が既存の宣言と一致することを確認してください。
// Foo.h
namespace llvm {
int foo(const char *s);
}
// Foo.cpp
#include "Foo.h"
using namespace llvm;
int llvm::foo(const char *s) {
// ...
}
これを行うことで、定義がヘッダーから宣言されたものと一致しないバグを回避できます。たとえば、次のC++コードは、ヘッダーで宣言された既存の関数の定義を提供する代わりに、llvm::foo
の新しいオーバーロードを定義します。
// Foo.cpp
#include "Foo.h"
namespace llvm {
int foo(char *s) { // Mismatch between "const char *" and "char *"
}
} // namespace llvm
このエラーは、リンカーが元の関数の使用の定義を見つけられない場合、ビルドがほぼ完了するまで検出されません。関数が名前空間修飾子を使用して定義されている場合、エラーは定義がコンパイルされたときにすぐに検出されます。
クラスメソッドの実装はすでにクラスに名前を付ける必要があり、新しいオーバーロードをインライン展開ではない方法で導入することはできないため、この推奨事項はそれらには適用されません。
早期終了とcontinue
を使用してコードを簡素化する¶
コードを読む際には、コードのブロックを理解するために、読者がどれだけ多くの状態と以前の決定を覚えている必要があるかを考慮してください。コードの理解を難しくしない限り、インデントをできる限り減らすことを目指してください。これを行うための優れた方法の1つは、長いループで早期終了とcontinue
キーワードを使用することです。早期終了を使用しない次のコードを考えてください。
Value *doSomething(Instruction *I) {
if (!I->isTerminator() &&
I->hasOneUse() && doOtherThing(I)) {
... some long code ....
}
return 0;
}
このコードは、'if'
の本体が大きい場合、いくつかの問題があります。関数の先頭を見ると、これは**非終端命令に対してのみ**興味深い処理を行い、他の述語を持つものに対してのみ適用されることがすぐにわかりません。第二に、if
文によりコメントの配置が難しくなるため、これらの述語がなぜ重要であるかを(コメントで)説明するのは比較的困難です。第三に、コードの深い部分にいるときは、インデントがさらに1レベル深くなっています。最後に、関数の先頭を読んでいるとき、述語が真ではない場合の結果が何であるかはわかりません。その結果を知るには、関数の最後まで読む必要があります。
このようにコードをフォーマットすることを強くお勧めします。
Value *doSomething(Instruction *I) {
// Terminators never need 'something' done to them because ...
if (I->isTerminator())
return 0;
// We conservatively avoid transforming instructions with multiple uses
// because goats like cheese.
if (!I->hasOneUse())
return 0;
// This is really just here for example.
if (!doOtherThing(I))
return 0;
... some long code ....
}
これにより、これらの問題が解決されます。同様の問題は、for
ループで頻繁に発生します。愚かな例としては、次のようなものがあります。
for (Instruction &I : BB) {
if (auto *BO = dyn_cast<BinaryOperator>(&I)) {
Value *LHS = BO->getOperand(0);
Value *RHS = BO->getOperand(1);
if (LHS != RHS) {
...
}
}
}
非常に小さなループであれば、このような構造で問題ありません。しかし、10~15行を超えると、一目で読み解くことが難しくなります。このようなコードの問題点は、ネストが非常に速やかに深くなることです。つまり、コードの読者は、`if`条件に`else`があるかどうかなどがわからないため、ループ内で何が起こっているかをすぐに把握するために多くのコンテキストを頭の中に保持しておく必要があります。
for (Instruction &I : BB) {
auto *BO = dyn_cast<BinaryOperator>(&I);
if (!BO) continue;
Value *LHS = BO->getOperand(0);
Value *RHS = BO->getOperand(1);
if (LHS == RHS) continue;
...
}
これは、関数の早期終了を使用することのメリットをすべて備えています。ループのネストを減らし、条件が真である理由を説明しやすくし、読者にとって`else`が続くことを考慮する必要がないことを明確にします。ループが大きい場合、これは理解しやすさを大きく向上させることができます。
`return`の後に`else`を使用しないでください¶
上記と同様の理由(インデントの削減と読みやすさの向上)から、制御フローを中断する何か(`return`、`break`、`continue`、`goto`など)の後に`'else'`または`'else if'`を使用しないでください。例:
case 'J': {
if (Signed) {
Type = Context.getsigjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_sigjmp_buf;
return QualType();
} else {
break; // Unnecessary.
}
} else {
Type = Context.getjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_jmp_buf;
return QualType();
} else {
break; // Unnecessary.
}
}
}
このように記述する方が優れています。
case 'J':
if (Signed) {
Type = Context.getsigjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_sigjmp_buf;
return QualType();
}
} else {
Type = Context.getjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_jmp_buf;
return QualType();
}
}
break;
あるいは、この場合はさらに(以下のように)
case 'J':
if (Signed)
Type = Context.getsigjmp_bufType();
else
Type = Context.getjmp_bufType();
if (Type.isNull()) {
Error = Signed ? ASTContext::GE_Missing_sigjmp_buf :
ASTContext::GE_Missing_jmp_buf;
return QualType();
}
break;
アイデアは、インデントとコードを読む際に追跡する必要があるコードの量を減らすことです。
注:このアドバイスは`constexpr if`文には適用されません。`else`句のサブステートメントは破棄されたステートメントになる可能性があるため、`else`を削除すると予期しないテンプレートインスタンス化が発生する可能性があります。したがって、次の例は正しいです。
template<typename T>
static constexpr bool VarTempl = true;
template<typename T>
int func() {
if constexpr (VarTempl<T>)
return 1;
else
static_assert(!VarTempl<T>);
}
述語ループを述語関数に変換する¶
ブール値を計算するだけの小さなループを書くことは非常に一般的です。人々が一般的にこれらを書く方法はいくつかありますが、この種のものの例は次のとおりです。
bool FoundFoo = false;
for (unsigned I = 0, E = BarList.size(); I != E; ++I)
if (BarList[I]->isFoo()) {
FoundFoo = true;
break;
}
if (FoundFoo) {
...
}
このようなループの代わりに、(静的である可能性のある)述語関数を使用して、早期終了を使用することをお勧めします。
/// \returns true if the specified list has an element that is a foo.
static bool containsFoo(const std::vector<Bar*> &List) {
for (unsigned I = 0, E = List.size(); I != E; ++I)
if (List[I]->isFoo())
return true;
return false;
}
...
if (containsFoo(BarList)) {
...
}
これを行う理由はたくさんあります。インデントを減らし、同じ述語をチェックする他のコードで共有できることが多いコードを分離します。さらに重要なのは、関数名を選択することを強制し、コメントを記述することを強制することです。この単純な例では、これはそれほど価値を追加しません。しかし、条件が複雑な場合、これにより、読者がこの述語を照会するコードを理解しやすくなります。BarListにfooが含まれているかどうかを確認する方法の詳細をインラインで表示する代わりに、関数名を信頼して、より優れた局所性で読み続けることができます。
低レベルの問題¶
型、関数、変数、列挙子を適切に命名する¶
名前の選択が不適切だと、読者を誤解させ、バグの原因となる可能性があります。記述的な名前を使用することがいかに重要であるかを十分に強調することはできません。妥当な範囲内で、基礎となるエンティティのセマンティクスと役割に一致する名前を選択してください。よく知られている場合を除き、略語は避けてください。適切な名前を選択したら、名前の一貫した大文字小文字を使用してください。一貫性がないと、クライアントはAPIを暗記するか、正確なスペルを見つけるために検索する必要があります。
一般的に、名前はキャメルケース(例:`TextFileReader`と`isLValue()`)にする必要があります。異なる種類の宣言には異なるルールがあります。
**型名**(クラス、構造体、列挙型、typedefなど)は名詞で、大文字で始める必要があります(例:`TextFileReader`)。
**変数名**は名詞である必要があります(状態を表すため)。名前はキャメルケースで、大文字で始める必要があります(例:`Leader`または`Boats`)。
**関数名**は動詞句である必要があります(アクションを表すため)。コマンドのような関数は命令形にする必要があります。名前はキャメルケースで、小文字で始める必要があります(例:`openFile()`または`isFoo()`)。
**列挙型宣言**(例:`enum Foo {...}`)は型なので、型の命名規則に従う必要があります。列挙型の一般的な用途は、共用体の識別子またはサブクラスのインジケーターです。列挙型がこのような用途で使用される場合、`Kind`サフィックスが必要です(例:`ValueKind`)。
**列挙子**(例:`enum { Foo, Bar }`)と**パブリックメンバ変数**は、型と同様に大文字で始める必要があります。列挙子が独自の小さな名前空間内で定義されているか、クラス内で定義されている場合を除き、列挙子は列挙型宣言名に対応するプレフィックスが必要です。たとえば、`enum ValueKind {...};`には`VK_Argument`、`VK_BasicBlock`などの列挙子を含めることができます。単なる便宜的な定数である列挙子は、プレフィックスの要件から除外されます。例えば
enum { MaxSize = 42, Density = 12 };
例外として、STLクラスを模倣するクラスは、STLのスタイル(小文字の単語をアンダースコアで区切る)でメンバ名を持つことができます(例:`begin()`、`push_back()`、`empty()`)。複数のイテレータを提供するクラスは、`begin()`と`end()`に単数のプレフィックスを追加する必要があります(例:`global_begin()`と`use_begin()`)。
いくつかの例を以下に示します。
class VehicleMaker {
...
Factory<Tire> F; // Avoid: a non-descriptive abbreviation.
Factory<Tire> Factory; // Better: more descriptive.
Factory<Tire> TireFactory; // Even better: if VehicleMaker has more than one
// kind of factories.
};
Vehicle makeVehicle(VehicleType Type) {
VehicleMaker M; // Might be OK if scope is small.
Tire Tmp1 = M.makeTire(); // Avoid: 'Tmp1' provides no information.
Light Headlight = M.makeLight("head"); // Good: descriptive.
...
}
アサーションを積極的に使用する¶
「`assert`」マクロを最大限に活用してください。すべての事前条件と仮定を確認してください。バグ(必ずしもあなたのものではない)がアサーションによって早期に検出される可能性があり、デバッグ時間を大幅に削減できるためです。「`
デバッグをさらに支援するために、アサーションステートメントに何らかのエラーメッセージを入れてください。これは、アサーションがトリップされた場合に表示されます。これにより、貧弱なデバッガーは、なぜアサーションが行われ、強制されているのか、そしておそらくそれについてどうすればよいかを理解するのに役立ちます。完全な例を以下に示します。
inline Value *getOperand(unsigned I) {
assert(I < Operands.size() && "getOperand() out of range!");
return Operands[I];
}
さらに例を示します。
assert(Ty->isPointerType() && "Can't allocate a non-pointer type!");
assert((Opcode == Shl || Opcode == Shr) && "ShiftInst Opcode invalid!");
assert(idx < getNumSuccessors() && "Successor # out of range!");
assert(V1.getType() == V2.getType() && "Constant types must be identical!");
assert(isa<PHINode>(Succ->front()) && "Only works on PHId BBs!");
お分かりいただけたと思います。
過去には、アサーションは到達してはならないコードの部分を示すために使用されていました。これらは通常、次の形式でした。
assert(0 && "Invalid radix for integer literal");
これにはいくつかの問題があり、主な問題は、一部のコンパイラがアサーションを理解しない場合や、アサーションがコンパイルされないビルドで戻り値がないことを警告する場合があることです。
今日では、はるかに優れたものがあります。`llvm_unreachable`
llvm_unreachable("Invalid radix for integer literal");
アサーションが有効な場合、到達した場合にメッセージが表示され、プログラムが終了します。アサーションが無効な場合(つまり、リリースビルドの場合)、`llvm_unreachable`はコンパイラに対してこのブランチのコード生成をスキップするというヒントになります。コンパイラがこの機能をサポートしていない場合、「abort」の実装にフォールバックします。
`llvm_unreachable`を使用して、到達してはならないコードの特定のポイントをマークします。これは、到達できないブランチに関する警告に対処するのに特に望ましいですが、特定のコードパスに到達することが無条件に何らかのバグ(ユーザー入力に起因しないもの。以下を参照)である場合にいつでも使用できます。`assert`の使用には、常にテスト可能な述語を含める必要があります(`assert(false)`とは対照的に)。
エラー状態がユーザー入力によってトリガーされる可能性がある場合は、LLVMプログラマーズマニュアルで説明されている回復可能なエラーメカニズムを代わりに使用する必要があります。これが現実的ではない場合は、`report_fatal_error`を使用できます。
もう1つの問題は、アサーションでのみ使用される値が、アサーションが無効な場合に「未使用の値」警告を生成することです。たとえば、このコードは警告を生成します。
unsigned Size = V.size();
assert(Size > 42 && "Vector smaller than it should be");
bool NewToSet = Myset.insert(Value);
assert(NewToSet && "The value shouldn't be in the set yet");
これらは2つの興味深い異なるケースです。最初のケースでは、`V.size()`への呼び出しはアサーションに対してのみ有用であり、アサーションが無効な場合に実行したくありません。このようなコードでは、呼び出しをアサーション自体に移動する必要があります。2番目のケースでは、呼び出しの副作用は、アサーションが有効かどうかにかかわらず発生する必要があります。この場合、値をvoidにキャストして警告を無効にする必要があります。具体的には、次のようにコードを記述することをお勧めします。
assert(V.size() > 42 && "Vector smaller than it should be");
bool NewToSet = Myset.insert(Value); (void)NewToSet;
assert(NewToSet && "The value shouldn't be in the set yet");
`using namespace std`を使用しないでください¶
LLVMでは、「`using namespace std;`」に依存するのではなく、標準名前空間からのすべての識別子を「`std::`」プレフィックスで明示的にプレフィックスすることを好みます。
ヘッダーファイルで「`using namespace XXX`」ディレクティブを追加すると、ヘッダーを`#include`するソースファイルの名前空間が汚染され、保守上の問題が発生します。
実装ファイル(例: `.cpp` ファイル)では、このルールはスタイルに関するルールに近いですが、それでも重要です。基本的には、明示的なネームスペース接頭辞を使用することで、どの機能が使用され、どこから来ているのかがすぐに分かりやすくなり、コードが**より明確**になります。また、LLVMコードと他のネームスペースとの間でネームスペースの衝突が発生しないため、**移植性が高まります**。移植性のルールは重要です。なぜなら、異なる標準ライブラリの実装では異なるシンボル(本来公開されるべきではないものも含む)が公開され、C++標準の将来の改訂では`std` ネームスペースにさらに多くのシンボルが追加される可能性があるからです。そのため、LLVMでは`'using namespace std;'`は決して使用しません。
この一般的なルールに対する例外(つまり、`std` ネームスペースに対する例外ではありません)は、実装ファイルの場合です。例えば、LLVMプロジェクトのすべてのコードは`llvm` ネームスペース内に存在するコードを実装しています。そのため、`.cpp` ファイルの先頭で`#include` の後に`'using namespace llvm;'`ディレクティブを使用することが許容され、実際により明確になります。これにより、中括弧に基づいてインデントを行うソースエディタにおけるファイル本体のインデントが減少し、概念的なコンテキストがよりクリーンに保たれます。このルールの一般的な形式は、任意のネームスペースでコードを実装する`.cpp` ファイルは、そのネームスペース(およびその親ネームスペース)を使用できますが、他のネームスペースは使用してはならないというものです。
ヘッダー内のクラスに対する仮想メソッドアンカーの提供¶
クラスがヘッダーファイルで定義されており、vtableを持つ場合(仮想メソッドを持つ、または仮想メソッドを持つクラスから派生する場合)、クラスには常に少なくとも1つのアウトオブラインの仮想メソッドが必要です。これがないと、コンパイラはvtableとRTTIを`#include` するすべての`.o` ファイルにコピーするため、`.o` ファイルのサイズが肥大化し、リンク時間が長くなります。
列挙型を完全に網羅したswitch文でdefaultラベルを使用しない¶
`-Wswitch` は、defaultラベルのない列挙型に対するswitch文がすべての列挙値を網羅していない場合に警告します。列挙型を完全に網羅したswitch文にdefaultラベルを記述すると、その列挙型に新しい要素が追加された場合でも`-Wswitch`警告は発生しません。このようなdefaultラベルの追加を防ぐために、Clangには`-Wcovered-switch-default`警告があります。これはデフォルトでは無効ですが、この警告をサポートするバージョンのClangでLLVMをビルドすると有効になります。
このスタイル要件の波及効果として、列挙型を網羅したswitch文の各caseからreturnする場合、GCCでLLVMをビルドすると「制御がvoidではない関数の末尾に到達する可能性がある」という警告が表示されることがあります。これは、GCCがenum式が個々の列挙子の値だけでなく、表現可能な任意の値をとる可能性があると想定しているためです。この警告を抑制するには、switchの後に`llvm_unreachable`を使用します。
可能な限り範囲ベースの`for`ループを使用する¶
C++11で導入された範囲ベースの`for`ループにより、イテレータの明示的な操作はほとんど不要になりました。新しく追加されたコードについては、可能な限り範囲ベースの`for`ループを使用します。例:
BasicBlock *BB = ...
for (Instruction &I : *BB)
... use I ...
`std::for_each()` / `llvm::for_each()`関数の使用は、呼び出し可能オブジェクトが既に存在する場合を除いて推奨されません。
ループのたびに`end()`を評価しない¶
範囲ベースの`for`ループを使用できない場合、明示的なイテレータベースのループを記述する必要がある場合、`end()`がループの各反復で再評価されているかどうかを注意深く確認してください。よくある間違いは、次のスタイルでループを記述することです。
BasicBlock *BB = ...
for (auto I = BB->begin(); I != BB->end(); ++I)
... use I ...
この構成の問題は、「`BB->end()`」がループのたびに評価されることです。このようにループを記述する代わりに、ループが開始される前に一度だけ評価するようにループを記述することを強く推奨します。これを行う便利な方法は次のとおりです。
BasicBlock *BB = ...
for (auto I = BB->begin(), E = BB->end(); I != E; ++I)
... use I ...
注意深い読者であれば、すぐにこれらの2つのループには異なるセマンティクスがあることに気付くでしょう。コンテナ(この場合は基本ブロック)が変更されている場合、「`BB->end()`」はループのたびに値が変更される可能性があり、2番目のループは実際には正しくない可能性があります。実際にこの動作に依存する場合は、最初の形式でループを記述し、意図的に行ったことを示すコメントを追加してください。
なぜ2番目の形式(正しい場合)を優先するのでしょうか?最初の形式でループを記述することには2つの問題があります。まず、ループの開始時に評価するよりも効率が悪い可能性があります。この場合、コストはおそらく軽微です(ループのたびに数回の追加ロード)。しかし、基本式がより複雑な場合、コストは急速に上昇する可能性があります。私は、終了式が「`SomeMap[X]->end()`」のようなものであったループを見たことがあり、マップのルックアップは実際には安価ではありません。2番目の形式で一貫して記述することで、問題全体を排除し、考える必要すらなくなります。
2番目の問題(さらに大きな問題)は、最初の形式でループを記述すると、コンテナが変更されていることを読者に示唆することです(コメントで簡単に確認できます!)。2番目の形式でループを記述すると、ループの本体を見なくても、コンテナが変更されていないことがすぐに明らかになり、コードの読みやすさと動作の理解が容易になります。
2番目のループ形式はキーストロークがいくつか多くなりますが、強く推奨します。
`` のインクルードは禁止 ¶
ライブラリファイルで`#include
他のストリームヘッダー(例えば`
注記
新しいコードでは、書き込みには常にraw_ostreamを、ファイルの読み取りには`llvm::MemoryBuffer` APIを使用する必要があります。
`raw_ostream`を使用する¶
LLVMには、`llvm/Support/raw_ostream.h`に軽量でシンプルで効率的なストリーム実装が含まれており、`std::ostream`の一般的な機能をすべて提供します。すべての新しいコードでは、`ostream`の代わりに`raw_ostream`を使用する必要があります。
`std::ostream`とは異なり、`raw_ostream`はテンプレートではなく、`class raw_ostream`として前方宣言できます。公開ヘッダーは一般的に`raw_ostream`ヘッダーを含めるべきではありませんが、前方宣言と`raw_ostream`インスタンスへの定数参照を使用する必要があります。
`std::endl`を避ける¶
`std::endl`修飾子は、`iostreams`で使用する場合、指定された出力ストリームに出力行を出力します。しかし、これに加えて、出力ストリームをフラッシュします。つまり、これらは同等です。
std::cout << std::endl;
std::cout << '\n' << std::flush;
ほとんどの場合、出力ストリームをフラッシュする理由はほとんどないため、リテラル`'\n'`を使用する方が良いでしょう。
クラス定義内で関数を定義するときに`inline`を使用しない¶
クラス定義内で定義されたメンバ関数は暗黙的にインラインになるため、この場合`inline`キーワードを記述しないでください。
しない
class Foo {
public:
inline void bar() {
// ...
}
};
する
class Foo {
public:
void bar() {
// ...
}
};
ミクロの詳細¶
このセクションでは、優先される低レベルのフォーマットガイドラインと、それらを優先する理由について説明します。
括弧の前後の空白¶
制御フロー文の場合のみ、開き括弧の前に空白を入れます。通常の関数呼び出し式や関数ライクなマクロには入れません。例:
if (X) ...
for (I = 0; I != 100; ++I) ...
while (LLVMRocks) ...
somefunc(42);
assert(3 != 4 && "laws of math are failing me");
A = foo(42, 92) + bar(X);
このスタイルを採用する理由は、完全に恣意的ではありません。このスタイルにより、制御フロー演算子がより目立ち、式の流れが向上します。
プリインクリメントを優先する¶
厳格なルール: プリインクリメント(++X
)は、ポストインクリメント(X++
)よりも遅くなく、場合によってははるかに高速です。可能な限りプリインクリメントを使用してください。
ポストインクリメントのセマンティクスには、インクリメントされる値のコピーを作成し、それを返し、その後「作業値」をプリインクリメントすることが含まれます。プリミティブ型の場合、これは大した問題ではありません。しかし、イテレータの場合、大きな問題になる可能性があります(例:一部のイテレータにはスタックやセットオブジェクトが含まれており…イテレータのコピーはこれらでもコピーコンストラクタを呼び出す可能性があります)。一般的に、常にプリインクリメントを使用する習慣を身につければ、問題はありません。
名前空間のインデント¶
一般的に、可能な限りインデントを減らすよう努めます。これは、コードを80カラムに収めるために必要であるだけでなく、コードの理解を容易にするためにも役立ちます。これを容易にし、場合によっては非常に深いネストを避けるために、名前空間をインデントしないでください。可読性を向上させるために、}
で閉じられている名前空間を示すコメントを追加しても構いません。例:
namespace llvm {
namespace knowledge {
/// This class represents things that Smith can have an intimate
/// understanding of and contains the data associated with it.
class Grokable {
...
public:
explicit Grokable() { ... }
virtual ~Grokable() = 0;
...
};
} // namespace knowledge
} // namespace llvm
閉じている名前空間が何らかの理由で明らかな場合は、終了コメントを省略しても構いません。たとえば、ヘッダーファイルの一番外側の名前空間は、めったに混乱の原因にはなりません。しかし、ソースファイル内で匿名および名前付きの両方の名前空間がファイルの途中で閉じられている場合は、明確化が必要になる可能性があります。
匿名名前空間¶
一般的に名前空間について説明した後、特に匿名名前空間について疑問に思うかもしれません。匿名名前空間は、C++コンパイラに、名前空間の内容が現在の翻訳単位内でのみ見えることを伝える優れた言語機能であり、より積極的な最適化が可能になり、シンボル名の衝突の可能性が排除されます。匿名名前空間は、C++における「static」がC関数とグローバル変数に対するものと同じです。「static
」はC++でも使用できますが、匿名名前空間はより一般的です。ファイルに対してクラス全体をプライベートにすることができます。
匿名名前空間の問題は、自然と本文のインデントを促し、参照の局所性を低下させることです。C++ファイルでランダムな関数定義を見つけた場合、それがstaticとマークされているかどうかは簡単にわかりますが、匿名名前空間内にあるかどうかを確認するには、ファイルの大部分をスキャンする必要があります。
そのため、簡単なガイドラインがあります。匿名名前空間をできるだけ小さくし、クラス宣言のみに使用します。例:
namespace {
class StringSort {
...
public:
StringSort(...)
bool operator<(const char *RHS) const;
};
} // namespace
static void runHelper() {
...
}
bool StringSort::operator<(const char *RHS) const {
...
}
クラス以外の宣言を匿名名前空間に入れるのは避けてください。
namespace {
// ... many declarations ...
void runHelper() {
...
}
// ... many declarations ...
} // namespace
大きなC++ファイルの途中で「runHelper
」を見た場合、この関数がファイルローカルかどうかをすぐに判断する方法がありません。対照的に、関数がstaticとマークされている場合、関数がローカルであることを知るためにファイルの遠く離れた場所を参照する必要はありません。
if/else/ループ文の単純な単文本体に中括弧を使用しない¶
if
、else
、またはfor/whileループ文の本体を作成する場合、不要な行のノイズを避けるために、中括弧を省略することを好みます。ただし、中括弧の省略がコードの可読性と保守性を損なう場合は、中括弧を使用する必要があります。
単一文にコメントが付けられている場合(コメントをif
またはループ文の上に移動できないと仮定します。以下を参照)に中括弧を省略すると、可読性が損なわれると判断しています。
同様に、単一文の本体が複雑すぎて、次の文を含むブロックがどこで始まったのかが分かりにくい場合も、中括弧を使用する必要があります。if
/else
チェーンまたはループはこの規則では単一文とみなされ、この規則は再帰的に適用されます。
このリストは網羅的ではありません。たとえば、if
/else
チェーンがすべてのメンバーについて、またはいずれについても中括弧付きの本体を使用していない場合、または複雑な条件式、深いネストなどがある場合にも、可読性が損なわれます。以下の例は、いくつかのガイドラインを提供することを意図しています。
保守性が損なわれるのは、if
の本体が(直接的または間接的に)else
のない入れ子になったif
文で終わる場合です。外側のif
に中括弧を使用すると、「ぶら下がりelse」が発生するのを防ぐのに役立ちます。
// Omit the braces since the body is simple and clearly associated with the
// `if`.
if (isa<FunctionDecl>(D))
handleFunctionDecl(D);
else if (isa<VarDecl>(D))
handleVarDecl(D);
// Here we document the condition itself and not the body.
if (isa<VarDecl>(D)) {
// It is necessary that we explain the situation with this surprisingly long
// comment, so it would be unclear without the braces whether the following
// statement is in the scope of the `if`.
// Because the condition is documented, we can't really hoist this
// comment that applies to the body above the `if`.
handleOtherDecl(D);
}
// Use braces on the outer `if` to avoid a potential dangling `else`
// situation.
if (isa<VarDecl>(D)) {
if (shouldProcessAttr(A))
handleAttr(A);
}
// Use braces for the `if` block to keep it uniform with the `else` block.
if (isa<FunctionDecl>(D)) {
handleFunctionDecl(D);
} else {
// In this `else` case, it is necessary that we explain the situation with
// this surprisingly long comment, so it would be unclear without the braces
// whether the following statement is in the scope of the `if`.
handleOtherDecl(D);
}
// This should also omit braces. The `for` loop contains only a single
// statement, so it shouldn't have braces. The `if` also only contains a
// single simple statement (the `for` loop), so it also should omit braces.
if (isa<FunctionDecl>(D))
for (auto *A : D.attrs())
handleAttr(A);
// Use braces for a `do-while` loop and its enclosing statement.
if (Tok->is(tok::l_brace)) {
do {
Tok = Tok->Next;
} while (Tok);
}
// Use braces for the outer `if` since the nested `for` is braced.
if (isa<FunctionDecl>(D)) {
for (auto *A : D.attrs()) {
// In this `for` loop body, it is necessary that we explain the situation
// with this surprisingly long comment, forcing braces on the `for` block.
handleAttr(A);
}
}
// Use braces on the outer block because there are more than two levels of
// nesting.
if (isa<FunctionDecl>(D)) {
for (auto *A : D.attrs())
for (ssize_t i : llvm::seq<ssize_t>(count))
handleAttrOnDecl(D, A, i);
}
// Use braces on the outer block because of a nested `if`; otherwise the
// compiler would warn: `add explicit braces to avoid dangling else`
if (auto *D = dyn_cast<FunctionDecl>(D)) {
if (shouldProcess(D))
handleVarDecl(D);
else
markAsIgnored(D);
}
参照¶
これらのコメントや推奨事項の多くは、他のソースから集められています。私たちの仕事にとって特に重要な2冊の本は次のとおりです。
Effective C++ (スコット・マイヤーズ著)。同じ著者による「More Effective C++」と「Effective STL」も興味深く、役立ちます。
Large-Scale C++ Software Design (ジョン・ラコス著)
時間があれば、まだ読んでいない場合は読んでみてください。何か学ぶことがあるかもしれません。
コメントのフォーマット¶
一般的に、C++スタイルのコメント(通常のコメントには
//
、doxygen
ドキュメントコメントには///
)を優先してください。ただし、Cスタイル(/* */
)コメントを使用する必要があるケースがいくつかあります。C89と互換性のあるCコードを作成する場合。
Cソースファイルによって
#include
される可能性のあるヘッダーファイルを作成する場合。Cスタイルのコメントのみを受け付けるツールで使用されるソースファイルを作成する場合。
呼び出しにおける実際の引数として使用される定数の重要性を文書化する場合。これは
bool
パラメータ、または0
またはnullptr
を渡す場合に最も役立ちます。コメントには、意味のあるパラメータ名を含める必要があります。たとえば、次の呼び出しではパラメータの意味が明確ではありません。インラインのCスタイルコメントを使用すると、意図が明らかになります。
大量のコードをコメントアウトすることは推奨されませんが、本当に必要になった場合(ドキュメント作成目的、またはデバッグ印刷の提案として)、
#if 0
と#endif
を使用してください。これらは適切にネストされ、一般的にCスタイルのコメントよりも動作が良好です。