LLVMコーディング標準
LLVMは主にC++で実装されたコンパイラ基盤です。
近年急速に普及が進んでおり、RustやSwiftのバックエンドとしても利用されています。
LLVMの一部としてリリースされているCファミリーのコンパイラClang
は、macOSやiOS、FreeBSD、OpenBSDの標準コンパイラとして採用されています。
本記事は、LLVMプロジェクトで用いられているコーディング標準(LLVM Coding Standards)のざっくり日本語訳です。
「組織内でのコーディング規約作成の参考にしたい」「clang-format
等のフォーマッタでLLVMスタイルが指定できるが、その内容を知りたい」といった読者を想定しています。
LLVMのメジャーリリースに合わせてこの記事も更新していく予定です。現在は15.0.0版に基づいています。原文の変更内容は記事末尾に軽くまとめています。
解釈誤りや分かりづらさの指摘は、コメントや編集リクエストでいただけたら幸いです。
前書き
この文書ではLLVMプロジェクトで用いられるコーディング標準について説明します。「どんな場合も従うべき絶対要件」となるようなコーディング標準はありませんが、コーディング標準は(LLVMのような)ライブラリ構造の大規模コードベースにとって特に重要です。
この文書はフォーマットや空白等の細かい指針も提供しますが、それらは絶対的な標準ではありません。どんな場合も以下の原則に従います。
原則:既存コードを修正/拡張する場合は、ソースの追いやすさと均一化のために既存のスタイルを使う。
一部のコードベースには本文書の標準から逸れる特別な理由があることに注意してください。たとえば libc++
ですが、これは命名規則等がC++標準で定められているためです。
コードベースにはここの命名規則等に従わないコードも含まれています。これは大量のコードを持ってきたばかりのためです。長期目標はコードベース全体が規則に沿うことですが、既存コードを大きく整形するパッチは明らかに望んでいません。一方、ほかの理由での変更時にそのクラスのメソッド名を直すことは合理的です。コードレビューしやすくするため、そういった変更はコミットを分けてください。
本ガイドラインの究極の目標は、私たちのコードベースの可読性と保守性を高めることです。
言語、ライブラリ、および標準
全体としては、規格に準拠したモダンでポータブルなC++コードを実装言語とします。LLVMや関連プロジェクトのソースコードの大半はC++コードですが、いくつかの部位ではCコードが使われています。これは環境の制約、歴史的な制限、もしくはサードパーティ製コードの利用に由来しています。
C++標準のバージョン
特に記載がない限り、LLVMサブプロジェクトはC++14標準を用いて、また不要なベンダー拡張は避けて書かれています。
とはいえ、ホストコンパイラとしてサポートする主要なツールチェイン1で使える機能に限定しています。
(Getting Started with the LLVM SystemのSoftware
セクションも参照のこと)
どのツールチェインも、サポートする言語機能の良い資料を提供しています。
- Clang: https://clang.llvm.org/cxx_status.html
- GCC: https://gcc.gnu.org/projects/cxx-status.html#cxx14
- MSVC: https://msdn.microsoft.com/en-us/library/hh567368.aspx2
C++標準ライブラリ
カスタムデータ構造を作る代わりに、C++標準ライブラリやLLVMサポートライブラリできる限り活用してください。LLVMと関連プロジェクトでは、標準ライブラリとLLVMサポートライブラリをできるだけ重視し頼ります。
LLVMサポートライブラリ(たとえばADT)は、標準ライブラリに見当たらない特殊なデータ構造や機能を実装します。それらライブラリでは通常llvm
名前空間で実装され、期待される標準インタフェース(あれば)に従います。
C++とLLVMサポートライブラリ両方が似た機能を提供しており、C++実装を優先する特段の理由がない場合は、一般にLLVMライブラリをお勧めします。たとえば、たいていはstd::map
やstd::unordered_map
よりもllvm::DenseMap
を、またstd::vector
ではなくllvm::SmallVector
を使うべきです。
I/Oストリームのようないくつかの標準機能はあえて避け、代わりにLLVMのストリームライブラリ(raw_ostream)を使います。これに関する詳細はLLVM Programmer's Manualにあります。
LLVMのデータ構造とそのトレードオフについての詳細は、Programmer's Manualの該当章を参照ください。
Go言語のガイドライン
Go言語で記述されたコードは、以降の書式ルールの対象にはなりません。その代わりに、 gofmt ツールによる整形を採用しています。
Goコードは慣習に倣うよう努めてください。Effective Go および Go Code Review Comments の2つが良いガイドラインとなります。
機械的なソースの問題
ソースコードのフォーマット
コメント
可読性と保守性を高めるため、コメントを入れてください。英文で、適切な句読点と大小文字で書いてください。コードがなにを行おうとしているのか、またなぜ(why)行おうとしているのかを説明することに焦点を絞り、微細に どうやるか(how) を書くことは避けてください。重要な事柄をいくつか示します。
ファイルのヘッダ
すべてのソースファイルには、ファイルの基本的な目的を説明するヘッダコメントが必要です。
//===-- llvm/Instruction.h - Instruction class definition -------*- C++ -*-===//
//
// The LLVM Compiler Infrastructure
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/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.
///
//===----------------------------------------------------------------------===//
1行目の "-*- C++ -*-
" は、EmacsにソースファイルがCではなくC++であることを教えます(Emacsはデフォルトで .h
ファイルをCとして扱います)。
このタグは、.cpp
ファイルでは不要です。最初の行にはファイル名と短い説明があります。
ファイルの次のセクションは、ファイルがどのライセンスの元でリリースされたかを簡潔に定義します。これにより、ソースコードがどのような条件の下で配布できるかが明確になります。そのため、どのような形であれ、変更してはいけません。
本体はDoxygenコメント(通常の//
ではなく///
コメントで識別されます)にてファイルの目的を説明します。最初の一文(または\brief
で始まる段落)は概要として使われます。追加情報は空白行で分けます。アルゴリズムの実装でベースとする論文や資料があれば、その参照を含めてください。
ヘッダガード
ヘッダファイルのガードは、ユーザーが#includeで使うパスを大文字に変え、パス区切りや拡張子区切りを'_'へ変えたものにします。
たとえば、ヘッダファイルllvm/include/llvm/Analysis/Utils/Local.h
は#include "llvm/Analysis/Utils/Local.h"
となります。ですのでガードはLLVM_ANALYSIS_UTILS_LOCAL_H
です。
クラス概要
クラスはオブジェクト指向設計の基本要素です。そのため、クラス定義にはそのクラスが何に使われどのように働くかを説明するコメントブロックが必要です。すべての重要なクラスにdoxygen
コメントブロックが必要です。
メソッド情報
メソッドとグローバル関数も文書化してください。ここでは、何をするかについての簡単なメモや、エッジケースでの挙動の説明のみがあれば十分です。読者はコードを読まずとも使い方を理解できる必要があります。
想定外の事態に何が起きるかについて触れるとよいでしょう。たとえばメソッドがnullを返した場合など。
コメント書式
通常は、C++スタイルのコメントを用います(普通のコメントに//
、doxygen
の文書化コメントに///
)。以下のようにCスタイル(/* */
)を用いたほうが良い場合もあります。
- C89互換のCコードファイルを書く場合。
- Cソースファイルから
#include
されるヘッダファイルを書く場合。 - Cスタイルのコメントしか受け付けないツール向けのファイルを各場合。
- 実引数での定数の意味を説明する場合。特に
bool
パラメータや0
、nullptr
で有用です。引数名(meaningfulである)を含めます。たとえば、この呼び出しでパラメータの意味は不明確です。
Object.emitName(nullptr);
インラインのCスタイルコメントは意味を明確にします。
Object.emitName(/*Prefix=*/nullptr);
大量のコードのコメントアウトがどうしても必要な場合(ドキュメント目的やデバッグプリント案等)は、 #if 0
と#endif
を使ってください。Cスタイルコメントよりもうまく働きます。
ドキュメントコメントでのDoxygenの使用
\file
コマンドを使い、標準のファイルヘッダをファイルレベルのコメントにします。
すべての公開インタフェース(publicクラス、メンバーと非メンバー関数)について説明する段落を含めます。API名を読み替えただけの記載は避けます。最初の一文(または\brief
で始まる段落)は概要として使われます。\brief
は目が滑るため、単一の文を使ってみてください。詳細な議論は段落を分けます。
段落内で引数名を参照するには、\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);
/// 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形式でなくても)。
コメントの先頭に関数名やクラス名をコピーしないでください。関数やクラスが文書化されていることは明らかであり、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() { ... }
エラーと警告メッセージ
訳注:ユーザーに出力するメッセージの指針です。LLVM開発者がコーディング中、コンパイラ警告に直面した場合の振る舞いに関しては別章に記載されています。
明確な診断メッセージは、ユーザーが入力の問題を特定して直すために重要です。簡潔で正しい英語の散文を用い、何を誤ったのかの理解に必要なコンテキストを示します。そして、ほかのツールでの一般的なエラーメッセージのスタイルに合わせるには、最初の文を小文字で始め、最後の文は(別のもので終わっている場合)ピリオドなしに終えます。ほかの句読点で終わる文、 "did you forget ';'?" などはそのままでよいでしょう。
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などでは、これに準拠していない既存のスタイルが含まれていることがあります。プロジェクト全体で一貫した別のスタイルが使われていれば、それを使います。それ以外では、この標準はすべてのLLVMツールに適用されます。clangやclang-tidyなども含みます。
ツールやプロジェクトで警告やエラーを発行する既存の関数がない場合は、Support/WithColor.h
で提供されるエラー/警告ハンドラを使って適切なスタイルで出力されるようにします。stderrには直接出力しません。
report_fatal_error
を使う場合、通常のエラーメッセージと同様の基準に従ってください。アサーションメッセージとllvm_unreachable
呼び出しでは自動でフォーマットされるため必ずしも同じスタイルに従う必要はなく、これらのガイドラインは当てはまらない場合があります。
#include
の形式
ファイルヘッダのコメント(およびヘッダファイルの場合はインクルードガード)直後に、そのファイルに必要最低限の#include
を並べます。#include
は次の順に並べます。
- メインモジュールヘッダ
- ローカル/プライベートヘッダ
- LLVMプロジェクト/サブプロジェクトのヘッダ(
clang/...
,lldb/...
,llvm/...
, ...) - システムの
#include
パスは省略せず、カテゴリごとに辞書順で並べます。
メインモジュールヘッダファイルは、.h
ファイルで定義されたインタフェースを実装する.cpp
ファイルに適用されます。この#include
は、それがファイルシステムのどこにあるかにかかわらず、最初にincludeされるべきです。.cpp
ファイルが実装するインタフェースをファイル先頭でincludeすることにより、ヘッダ内の#include
に含まれない依存関係がないことを確認できます。暗黙の依存関係があった場合、コンパイルエラーとなってくれます。また、.cpp
の実装するインタフェースがどこで定義されているかを示す一種のドキュメントにもなります。
LLVMプロジェクトとサブプロジェクトのヘッダでは、同様の理由で具体性の高いものから順にグループ分けします。たとえば、LLDBはclangとLLVMに依存しclangはLLVMに依存します。そのため、LLDBのソースファイルはlldb
、clang
、llvm
の順にヘッダファイルをインクルードします。これにより、LLDBヘッダファイルから必要なインクルードが漏れてしまう可能性を減らします。clangでも同様に、LLVMヘッダの前に独自ヘッダをインクルードします。このルールは、すべてのLLVMサブプロジェクトに適用されます。
ソースコードの幅
80桁に収めてください。
ディスプレイに複数のファイルを並べて表示するために、コード幅にはある程度の制限が必要です。その選択においては、多少恣意的ですが標準的なものを選ぶべきです。80桁の代わりたとえば90桁にしても、大した価値も得られず印刷にも不便です。また多くの他プロジェクトでは80桁が採用されているため、みなエディタをそのように設定しています。
空白
ソースファイルではタブよりもスペースがよいです。タブは表示環境ごとに異なるタブストップで展開され崩れる恐れがあります。
いつものように原則に従いましょう。既存コードに手を入れる場合、既存のスタイルに準じます。
末尾空白(trailing whitespace
)を追加しないでください。 よくあるエディタはファイル保存時に末尾の空白を自動的に削除するため、差分とコミットに無関係な変更が現れてしまいます。
ラムダはコードブロックと同様に整形
複数行のラムダは、コードブロックと同様に整形してください。もし文中に複数行のラムダひとつしかなく、その後に式もない場合、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;
});
このフォーマットを活かすため、新規APIで継続や単一の呼び出し可能な引数(関数オブジェクトやstd::function
)をとる場合、なるべく最後の引数にします。
文の中にいくつも複数行のラムダがあったり、ラムダの後ろに追加パラメータがある場合には、[]
から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
条件での代入はたいていtypoです。
if (V = getValue()) {
...
}
いくつかのコンパイラは上記のコードに警告します。括弧を足せば抑えられます。
if ((V = getValue())) {
...
}
移植性のあるコードを書く
ほとんどの場合、完全に移植性のあるコードを書けます。移植性のないコードに頼らざるを得ない場合は、明確に定義されよく文書化されたインタフェースの背後に置きます。
RTTIや例外を使わない
コードと実行ファイルのサイズを減らすために、LLVMでは例外やRTTI(実行時型情報、たとえばdynamic_cast<>
)は使いません。
とはいえ、LLVMではRTTIを手で展開した isa<>、cast<>、そしてdyn_cast<> のようなテンプレートを広く用います。RTTIのこの形式は、任意のクラスにオプトインで追加できます。
静的コンストラクタを使わない
静的コンストラクタとデストラクタ(たとえば、コンストラクタやデストラクタを持つ型のグローバル変数)はコードベースに追加されるべきではなく、可能な限り除かなければなりません。
異なるソースファイル内のグローバル変数は任意の順序で初期化されるため、コードの推測が難しくなります。
静的コンストラクタは、LLVMをライブラリとして使うプログラムの起動時間に悪影響を及ぼします。私たちは、追加のLLVMターゲットやアプリケーションのライブラリへのリンクがゼロコストであることを強く望みますが、静的コンストラクタはこの目標を覆します。
class
とstruct
キーワードの使い方
C++では、class
とstruct
キーワードはほぼ同じ意味で使えます。唯一の違いはクラス宣言の場合です。class
はデフォルトでメンバーがprivateですが、struct
はpublicです。
- 宣言と定義では、同じキーワードを使う必要があります。
class
での宣言に対しては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以降では「一般初期化構文(generalized initialization syntax)」があり、ブレース初期化子リストを使ってコンストラクタを呼べます。重要なロジックや特定のコンストラクタを呼び出したい場合、これらを使わないでください。それらは集約初期化というよりも括弧を使った関数呼び出しでしょう。同様に、名前の付いた型をその場で生成するためにコンストラクタを呼ぶ場合、ブレース初期化子リストを使わないでください。代わりに、集約等ではブレース初期化リスト(一時的な型を除く)を使います。
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
は有用です。std::vector<T>::iterator
のようなコンテナクラス内の型定義は抽象化されすぎている型の典型例でしょう。
同様に、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();
ポインタ順序による非決定性に注意
一般に、ポインタ間で順序はありません。その結果、setやmapのように順序のないコンテナで、キーにポインタが使われる場合、反復(iteration)順序は未定義です。したがって、そのようなコンテナの反復は結果として非決定的3なコードが生成されます。生成されたコードは正しく動く可能性がありますが、非決定性により再現しないバグを生じデバッグが難しくなる恐れもあります。
順序ある結果を期待する場合は、順序なしコンテナの反復前にソートしてください。それか、ポインタキーを反復したいならvector
/MapVector
/SetVector
のような順序付きコンテナを使います。
等しい要素のソートによる非決定性に注意
std::sort
は安定ソートではありません。そのため、等しい要素を持つコンテナにstd::sort
を使うと、非決定的な動作となる恐れがあります。
この非決定的な挙動を見つけるため、LLVMは新しいllvm::sortラッパ関数を導入しました。EXPENSIVE_CHECKSビルドの場合、ソート前にコンテナをランダムにシャッフルします。std::sort
ではなくllvm::sort
をデフォルトで使います。
スタイルの問題
高位の問題
自己完結型ヘッダ
ヘッダファイルは自己完結型(それのみでコンパイル)とし、.h
で終えます。
読み込みを意図した非ヘッダファイルは.inc
で終え、注意して使ってください。
すべてのヘッダは自己完結型にします。ユーザーとリファクタリングツールはincludeのために特別な条件に従う必要はありません。具体的には、ヘッダはインクルードガードを持ち、必要なすべてのヘッダをincludeします。
まれな例として、読み込みを意図したファイルは自己完結型ではありません。これらは通常、別のファイルの途中などでincludeされます。インクルードガードを使わなかったり、前提条件を含まない可能性があります。そのようなファイルには「.inc」拡張子を付けてください。控えめ使い、できるだけ自己完結型ヘッダファイルを優先してください。
一般的に、ヘッダは1つ以上の.cpp
ファイルで実装されます。これらの.cpp
ファイルは、始めにインタフェースを定義したヘッダをincludeします。これにより依存関係すべてが暗黙なく適切にヘッダに含まれることが保証されます。システムヘッダは翻訳単位のユーザーヘッダの後にincludeします。
ライブラリの階層化
ヘッダファイルのディレクトリ(たとえばinclude/llvm/Foo
)はライブラリ(Foo
)を定義します。あるライブラリ(ヘッダおよび実装)では依存関係リストにあるライブラリのもののみが使えます。
この制約が適用できるのは旧来のUnixリンカです(Mac & Windowsのリンカはlldと同様にこの制約を適用しません)。Unixリンカはコマンドラインで指定されたライブラリを左から右に検索します。ライブラリの循環依存は存在できません。
これはすべてのライブラリ間の依存を完全に強制するわけではありません。また重要なこととして、インライン関数によるヘッダファイルの循環依存は強制しません。
「これが正しく階層化されているか」に答える良い方法は、すべてのインライン関数がout-of-lineで定義された場合でもUnixリンカが成功するか考えてみることです。
(さらに依存関係の有効な順序すべてについて。リンク解決は線形のため、いくつかの暗黙の依存関係についてすり抜ける恐れがあります。AはBとCに依存するので、有効な順序は「C B A」や「B C A」です。どちらも利用の前に明示的な依存が来ます。ただし前者では暗黙的にBがCに依存している場合リンクが成功し、後者はその逆です)
#include
は最低限に
#include
はコンパイル時間を損ないます。どうしても必要でない場合は行わないでください、特にヘッダファイルでは。
でもちょっと待って! 使ったり、継承したりするためにクラス定義が必要になることがあります。その場合はどうぞ#include
してください。ですが、クラスの完全な定義が必要でない場合も多いことに注意してください。以下の場合、ヘッダファイルは不要です。
- クラスのポインタや参照を使うだけの場合
- 関数やメソッド宣言の戻り値で使うだけ(ヘッダにその関数定義を含まない)の場合
この勧めをやりすぎるのは簡単ですが、使っているヘッダファイルのすべてがインクルードされなくてはなりません。直接または別のヘッダファイルを介して間接的にそれらをインクルードできます。モジュールヘッダ内でのインクルード漏れを確認する方法があります。前述のように実装ファイルの最初にモジュールヘッダを含めるようにしてください。この方法により、隠れた依存関係がコンパイルエラーとなり発覚します。
「内部」ヘッダは非公開
多くのモジュールは、複数の実装(.cpp
)ファイルを使うことで複雑な実装を持っています。多くの場合、内部通信インタフェース(ヘルパークラス、余分な機能など)を公開モジュールヘッダファイルに置くことは魅力的です。でもやめて!
本当に必要な場合は、ソースファイルと同じディレクトリに非公開ヘッダファイルを置いて、それを内々でインクルードしてください。これは、非公開インタフェースが他者に乱されず非公開であることを保証します。
publicクラス自体に追加の実装メソッドを入れてもかまいません。private(またはprotected)とすることで、うまくいきます。
宣言された関数の実装には名前空間修飾子を用いる
ソースファイルで関数のアウトオブラインを実装する場合は、ソースファイルで名前空間ブロックを開かないでください。代わりに、名前空間修飾子を使い定義が既存の宣言と一致するようにします。次のようにします。
// 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つの方法は、早期終了する(Early Exits)ことと、長いループでcontinue
キーワードを使うことです。早期終了を使わない次のコードを考えてみます。
Value *doSomething(Instruction *I) {
if (!I->isTerminator() &&
I->hasOneUse() && doOtherThing(I)) {
... some long code ....
}
return 0;
}
'if'
の本文が大きい場合、このコードにはいくつか問題があります。第一に、関数の先頭を見ただけでは、条件に合わない場合何もしないことが分かりません。第二に、if
文はコメントしづらいレイアウトのため、なぜそれら述部が重要であるかコメントすることは割合難しいです。第三に、コード本体の深いところでは、余分にインデントされます。最後に、関数の先頭を見ただけでは条件に合わない場合、戻り値が何であるかは明らかではありません。nullを返すことを知るためには、関数の最後まで読まなければなりません。
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
を使用しない
上記と同様の理由(インデントの減少と読みやすさ)から、制御フローの中断後にelse
やelse if
を使わないでください。制御フローの中断とはreturn
、break
、continue
、goto
等です。
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;
この案はインデントと、コードを読み取るときに覚えておかなくてはならないコードの量を減らします。
Predicateはループから関数へ
成否判定だけの小さなループを書くことは非常に一般的です。これを書く方法は各種ありますが、たとえば次のようなものです。
bool FoundFoo = false;
for (unsigned I = 0, E = BarList.size(); I != E; ++I)
if (BarList[I]->isFoo()) {
FoundFoo = true;
break;
}
if (FoundFoo) {
...
}
この種のループの代わりに、早期終了するpredicate関数(staticの場合もあります)を使いましょう。
/// \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)) {
...
}
これを行うには多くの理由があります。インデントを減らし、しばしば共有できる同じチェックを行う別のコードとの重複を排除します。さらに重要なのは、関数の命名を強制し、それにコメントを書くことを強制します。このちっぽけな例では、大した価値がありません。ですが条件が複雑な場合は、predicateクエリをより簡単に理解できるようになるでしょう。インラインで詳細にBarListがfooを含むかをどのようにチェックするのかについて直面するのではなく、関数名を信頼しより良い局所性で読んでいけます。
低位の問題
型、関数、変数、および列挙子への適切な命名
下手に選ばれた名前は、読者に誤解を与え、バグを引き起こす可能性があります。私たちは、わかりやすい名前を使うことがどれだけ重要か、とても十分に強調しきれません。常識の範囲で、要素の意味と役割に一致する名前を選んでください。よく知られていない限り略語は避けてください。良い名前を選んだ後、名前に一貫した大文字を使ってください。ブレがあると、利用者はいちいち細かいスペルに煩わされます。
一般に、名前はキャメルケース(例:TextFileReader
とisLValue()
)でなければなりません。種類ごとにルールがあります。
-
型名(クラス、構造体、列挙型、typedef等を含む)は、名詞かつ大文字で始めます(例:
TextFileReader
)。 -
変数名は(状態を代表するような)名詞とします。名前はキャメルケースで、大文字で始めます(例:
Leader
やBoats
)。 -
関数名は(アクション表すような)動詞であるべきで、コマンドのような関数は命令型とします。名前はキャメルケースで、小文字で始めます(例:
openFile()
やisFoo()
)。 -
列挙型宣言(例:
enum Foo {...}
)は型のため、型名の規則に準じます。列挙型は一般に、共用体(union)の弁別や、サブクラスの情報提供のために使います。列挙型は、このような何かのために使われる場合、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
」マクロを最大限に使います。すべての前提条件と仮定をチェックすれば、バグ(あなたのものとは限りません)がアサーションによって早く発見できるとは限りませんが、デバッグ時間は劇的に減ります。「<cassert>
」ヘッダファイルは、おそらくもうインクルードされているので、追加のコストはかからないでしょう。
さらに、デバッグを支援するために、アサーション文に何らかのエラーメッセージを入れてください。これは、アサーションの発生原因とそれについて何をすべきかを、未熟なデバッガが理解する助けとなります。
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");
これにはいくつかの問題があります。主なものは、いくつかのコンパイラはアサーションを理解しない可能性があり、あるいはアサーションの部分でreturnが抜けていると警告を出すことです。
今日、私たちにはより良いものがあります。llvm_unreachable
。
llvm_unreachable("Invalid radix for integer literal");
アサーションを有効にすると、ここに到達した時点でメッセージを表示し、プログラムを終了します。アサーションが無効になっている場合(つまりリリースビルドでは)、llvm_unreachable
はこの分岐のコード生成は省略可能だというコンパイラへのヒントとなります。コンパイラがこれをサポートしていない場合は、「abort」実装にフォールバックされます。
llvm_unreachable
を使い到達してはならないコードの一点にマークします。これは到達しない分岐などの警告への対処として望ましいですが、使えるのはそこへの到達が無条件に何らかのバグ(ユーザーからの入力ではなく。以下を参照)となる場合です。
assert
の使用時は常にテスト可能なpredicate(assert(false)
とは異なります)を含める必要があります。
ユーザーの入力によりエラー状態となりうる場合は、代わりにLLVM Programmer's Manualで示す回復可能なエラーメカニズムを使う必要があります。それが実用的でない場合は、report_fatal_error
も使えます。
別の問題は、アサーションが無効になっている場合に、アサーションによってのみ使用される値で「未使用値」という警告が生成されるということです。
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()
の呼び出しはアサートのためにのみ有用であり、アサーションが無効になっている場合に実行されたくありません。このようなコードは、アサート自体に呼び出しを移動する必要があります。次の例では、呼び出しの副作用はアサートが有効かどうかによらず起きなければなりません。この場合、警告を無効にするには値を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」名前空間内のコードを実装します。ですので、それはOKとします。実際明解ですし、.cpp
ファイルは#include
直後の先頭に'using namespace llvm;'
ディレクティブがあります。これは、中括弧に基づいたインデントを行うソースエディタ向けに本文のインデントを減らし、概念的なコンテキストをきれいに保ちます。この規則を一般的に表すと、次のとおりです。
- 任意の名前空間内のコードを実装する任意の
.cpp
ファイルは、それの(そして親の)名前空間をusing
してもよい。 - 別の名前空間を
using
してはならない。
ヘッダ内クラスは仮想メソッドアンカーを提供する
クラスがヘッダファイル内で定義されvtableを持つ(仮想メソッドを持つか、そういったクラスから派生した)場合、仮想メソッドの少なくとも1つはout-of-line(.cppファイルで定義)します。これがないと、コンパイラは、そのヘッダを#include
した.o
ファイルすべてにvtableとRTTIをコピーし、.o
ファイルサイズとリンク時間を増やします。これはClangの-Wweak-vtables
警告で指摘されることがあります。
列挙型を網羅したswitchにdefaultを使わない
-Wswitch
は、列挙型の値を網羅せずdefaultもないswitchに警告を出します。列挙型を網羅したswitchにdefaultを書いた場合、新しい要素が列挙体に追加されても-Wswitch
は警告しません。この種のdefaultを追加することを避けるために、Clangは-Wcovered-switch-default
警告を持ちます。これはデフォルトで無効になっていますが、Clangの警告をサポートする版でLLVMをビルドする場合は有効になります。
この影響で、列挙型を網羅したswitchの各caseでreturnしていた場合、GCCでビルドすると「コントロールが非void型関数の終わりに到達します」関連の警告が出ます。GCCはenum句が個々の列挙子だけでなく任意の値を取れることを前提としているためです。この警告を抑止するには、switchの後にllvm_unreachable
を使います。
できるだけrange-based for
ループを使う
C++11でのrange-based for
ループの導入は、イテレータの明示的操作がめったにいらないことを意味します。私たちは、すべての新規追加コードに対して、できるだけrange-based for
ループを使います。
BasicBlock *BB = ...
for (Instruction &I : *BB)
... use I ...
呼び出し可能なオブジェクトがない場合を除いて、std::for_each()
/llvm::for_each()
関数の使用はお勧めしません。
ループで毎回end()
を評価しない
range-based 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つのループが異なるセマンティクスを持つ可能性にお気付きかもしれません。もしコンテナ(この例ではBasicBlock)が変更されるとしたら、"BB->end()
"はループ毎に変わるかもしれず、2つ目のループ(訳注:事前評価)は正しくないかもしれません。実際そのような挙動に依存している場合は、最初の形式でループを書き、「意図的に毎ループ評価している」旨コメント追加してください。
なぜ2つ目の形式がよいのか(正しい場合)? 最初の形式でループを書くことには2つの問題があります。第一に、ループ開始時に評価する方法と比べ、非効率かもしれません。この例では、コストはおそらくわずかですが、ループ毎に少し余分な負荷があります。しかしもっと複雑な式になると、コストが急上昇するかもしれません。"SomeMap[X]->end()
"のような式を見たことがあります。mapのルックアップはけっして安くありません。2つ目の書き方を一貫することで、問題を完全に排除でき、考えずに済みます。
さらに大きな第二の問題は、最初の形式で書くことはループ内でコンテナを変更していることを示すということです(コメントは簡単な確認という事実!)。2つ目の形式でループを書けば、コンテナは変更されないことがループ内を見ずとも分かります。
2つ目の形式でのループでは余分なキータイプはありますが、強くお勧めします。
#include <iostream>
禁止
ライブラリファイルで#include <iostream>
を使うことは禁止されています。なぜなら、多くの一般的な実装では、それを含むすべての変換単位に静的コンストラクタを透過的に注入するからです。
それ以外のストリームヘッダ(たとえば<sstream>
)の使用はこの点で問題ないことに注意してください。<iostream>
のみです。しかし、raw_ostream
の提供するさまざまなAPIは、ほとんどすべての用途でstd::ostream
スタイルのAPIよりも優れたパフォーマンスを発揮します。
新規コードでは常に、ファイル読み込みにllvm::MemoryBuffer
APIを、書き込みにraw_ostreamを使ってください。
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
修飾子は、iostream
とともに使われ、指定の出力ストリームに改行を出力します。そして、出力ストリームをFlashします。言い換えると、以下は同等です。
std::cout << std::endl;
std::cout << '\n' << std::flush;
ほとんどの場合、おそらく出力ストリームをFlashする理由はありません。'\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++`)よりも遅くなることはありません。むしろはるかに速くなる可能性があります。可能な限り前置インクリメントを使いましょう。
後置インクリメントは次の3つの内容を含みます。
- インクリメントされる値のコピーを作成する
- 「作業値」を前置インクリメントする
- インクリメント前の値を返す
プリミティブ型の場合、これは大きな問題ではありません。しかしイテレータでは、大きな問題となる可能性があります。たとえば、いくつかのイテレータはスタックを含み、それらにオブジェクトを設定します。イテレータをコピーすると、それらのコピーコンストラクタを呼ぶことにもなります。一般に、いつも前置インクリメントを使う習慣を身につれば、問題は起きません。
名前空間のインデント
通常、私たちは可能な限りインデントを減らすよう努めています。これはコードを過度な折り返しなしで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++の無名名前空間は、Cの関数とグローバル変数での「static」に似ています。C++でも「static
」は使えますが、無名名前空間のほうが一般的です。これはファイルに対してクラス全体を非公開にできます。
無名名前空間の問題は、本来的に本文のインデントを求めることと、参照の局所性を減らすことです。C++ファイルのrandom関数の定義を見る場合、それがstaticかどうかは簡単に分かります。無名名前空間にあるかどうかを知るには、ファイル全体を調べる必要があります。
このため、シンプルなガイドラインがあります。無名名前空間はできるだけ小さくし、クラス定義にのみ使います。
namespace {
class StringSort {
...
public:
StringSort(...)
bool operator<(const char *RHS) const;
};
} // anonymous namespace
static void runHelper() {
...
}
bool StringSort::operator<(const char *RHS) const {
...
}
クラス以外の宣言は匿名名前空間に入れません。
namespace {
// ... many declarations ...
void runHelper() {
...
}
// ... many declarations ...
} // anonymous namespace
大きなC++ファイルの途中の「runHelper
」を見た場合、ファイルローカルかどうかはすぐに判断できません。しかしstaticと明示されていれば、ローカルなのか知るためにファイル内の遠くを見なくて済みます。
単純なif/else/loop文では中括弧を使わない
if
/else
やfor/whileループ文の本体を書く場合、不要なラインノイズを避けるために中括弧を省くことが望ましいです。ただし、その省略によりコードの読みやすさ(readability)と保守性(maintainability)が損われる場合は中括弧を使わなくてはなりません。
読みやすさ(readability)が損なわれるのは、単一文にコメントがついている場合と考えられます(コメントをif
やループ文の前に巻き上げられないと仮定します。以下も参照)。
本体の単一文が十分に複雑な場合も同様で、文を含むブロックの始まりが分かりづらくなります。このような場合は中括弧を使います。このルールではif
/else
チェインやループも単一文とみなし、再帰的に適用します。
このリストは不十分です。たとえば、複雑な条件や深い入れ子などを持ったif
/else
チェインで中括弧をまばらに使うと読みにくくなります。以下の例でいくつかのガイドラインを示します。
保守性(maintainability)が損なわれるのは、if
の本体が(直接的/間接的に)ネストされたelse
なしのif
文で終わる場合です。外側のif
への中括弧は、「ぶら下がりelse(dangling else)」問題を避ける役に立ちます。
// 中括弧を省きます。本体は単純で、`if`との関係も明確です。
if (isa<FunctionDecl>(D))
handleFunctionDecl(D);
else if (isa<VarDecl>(D))
handleVarDecl(D);
// ここは条件についてのコメントです。本体部分についてではなく。
if (isa<VarDecl>(D)) {
// この驚くほど長いコメントで状況を説明する必要がありますが、
// 中括弧がないと次の文が`if`スコープ内かどうか分かりません。
// 既に条件のコメントがあるため、本体に関するこのコメントを
// `if`の前に巻き上げることはできません。
handleOtherDecl(D);
}
// 外側の`if`に中括弧を使い、ぶら下がり`else`の可能性を避けます。
if (isa<VarDecl>(D)) {
if (shouldProcessAttr(A))
handleAttr(A);
}
// `if`ブロックに中括弧を使い、`else`ブロックと同じ形を保ちます。
if (isa<FunctionDecl>(D)) {
handleFunctionDecl(D);
} else {
// この`else`の場合も、この驚くほど長いコメントで状況を
// 説明する必要がありますが、中括弧がないと次の文が
// `if`スコープ内かどうか分かりません。
handleOtherDecl(D);
}
// これは中括弧を省略するべきです。`for`ループは単一文しか含まないため、
// 中括弧を持つべきではありません。`if`も単一文(`for`ループ)しか含まないので、
// 同じく中括弧は省くべきです。
if (isa<FunctionDecl>(D))
for (auto *A : D.attrs())
handleAttr(A);
// `do-while`ループとそれを囲む文には中括弧を使います。
if (Tok->is(tok::l_brace)) {
do {
Tok = Tok->Next;
} while (Tok);
}
// ネストされた`for`が囲われているため、外側の`if`も囲みます。
if (isa<FunctionDecl>(D)) {
for (auto *A : D.attrs()) {
// この`for`ループ本文内では、この驚くほど長いコメントで状況を
// 説明し、`for`ブロックに中括弧を強制する必要があります。
handleAttr(A);
}
}
// 2階層以上のネストがあるため、外側のブロックに中括弧を使います。
if (isa<FunctionDecl>(D)) {
for (auto *A : D.attrs())
for (ssize_t i : llvm::seq<ssize_t>(count))
handleAttrOnDecl(D, A, i);
}
// ネストされた`if`の外側のブロックには中括弧を使います。
// さもないとコンパイラに警告されます:
// `add explicit braces to avoid dangling else`
if (auto *D = dyn_cast<FunctionDecl>(D)) {
if (shouldProcess(D))
handleVarDecl(D);
else
markAsIgnored(D);
}
関連項目
これらのコメントや勧告の多くはほかの情報源から抜粋されています。特に重要な書籍を紹介します。
- Effective C++ by Scott Meyers。同じ著者による「More Effective C++」「Effective STL」もまた、興味深く有用です。
- Large-Scale C++ Software Design by John Lakos
原文の変更内容
リンク先の更新や文言修正など内容に関わらない変更は記載省略してます。
14.0.0 -> 15.0.0
- 例拡充:スタイルの問題>細かい話>単純なif/else/loop文では中括弧を使わない
do-while
ループについて
コーディング標準としては記載がありませんが、サポートするツールチェインが変わっています。
Clang 3.5、Apple Clang 6.0、GCC 5.1、Visual Studio 2019
↓
Clang 5.0、Apple Clang 9.3、GCC 7.1、Visual Studio 2019 16.7
13.0.0 -> 14.0.0
変更なし。コーディング標準としては記載がありませんが、サポートするVisual Studioが2017->2019になっています。
12.0.0 -> 13.0.0
変更なし。
11.0.0 -> 12.0.0
- 追加:機械的なソースの問題>ソースコードのフォーマット>コメント>ヘッダガード
- 例拡充:スタイルの問題>細かい話>単純なif/else/loop文では中括弧を使わない
10.0.0 -> 11.0.0
- 追加:機械的なソースの問題>ソースコードのフォーマット>エラーと警告メッセージ
- 追加:スタイルの問題>高位の問題>宣言された関数の実装には名前空間修飾子を用いる
- 文言追加:スタイルの問題>低位の問題>できるだけrange-based
for
ループを使う
std::for_each()
/llvm::for_each()
の非推奨を明記 - 追加:スタイルの問題>細かい話>単純なif/else/loop文では中括弧を使わない
9.0.0 -> 10.0.0
ベースがC++11→C++14に変わりました。文章が大きく整理されました。
- 再構成:前書き>言語、ライブラリ、および標準
C++11→C++14に。 - 削除:機械的なソースの問題>ソースコードのフォーマット>インデントの一環
- 内容追加:機械的なソースの問題>言語とコンパイラの問題>コードを読みやすくするために
auto
型推論を使う
ジェネリックラムダについて。
8.0.0 -> 9.0.0
- 内容変更:機械的なソースの問題>ソースコードのフォーマット>ファイルのヘッダ
標準様式変更。ライセンスについて。
7.0.0 -> 8.0.0
- 内容追加:機械的なソースの問題>ソースコードのフォーマット>コメント書式
C++スタイルの例外として、パラメータの場合。 - 見出し変更と内容追加:機械的なソースの問題>ソースコードのフォーマット>空白(前版では「タブの代わりにスペースを使う」)
行末の空白について。
6.0.0 -> 7.0.0
- 追加:機械的なソースの問題>言語とコンパイラの問題>等しい要素のソートによる非決定性に注意
- 削除(下2つに分割):スタイルの問題>高位の問題>公開ヘッダファイルはモジュール
- 追加:スタイルの問題>高位の問題>自己完結型ヘッダ
- 追加:スタイルの問題>高位の問題>ライブラリの階層化
5.0.1 -> 6.0.0
- 追加:機械的なソースの問題>言語とコンパイラの問題>ポインタ順序による非決定性に注意
- 変更:スタイルの問題>高位の問題>早期終了と
continue
でコードをシンプルに
コードがシンプルに(range-based for, auto) - 内容追加:スタイルの問題>低位の問題>たっぷりのアサート
エラーの回復について。 - 追加:スタイルの問題>低位の問題>できるだけrange-based
for
ループを使う - 内容変更:スタイルの問題>低位の問題>ループで毎回
end()
を評価しない
この文書(翻訳)のライセンスについて
© Copyright 2003-2022, LLVM Project.
原文はこちらのライセンス下にあるLLVMのドキュメントに含まれているため、そちらのライセンスに従います。
翻訳者(@tenmyo)は著作権を主張しません。
-
訳注:LLVM15.0.0ではClang 5.0、Apple Clang 9.3、GCC 7.1、Visual Studio 2019 16.7。 ↩
-
訳注:対応すると思われる日本語ページ https://docs.microsoft.com/ja-jp/cpp/visual-cpp-language-conformance ↩
-
訳注:実行毎に順序が変わりうる。 ↩