LoginSignup
5
2

More than 5 years have passed since last update.

[翻訳] LLVMコーディング標準3.9.1(3/4)機械的なソースの問題

Last updated at Posted at 2017-03-05

この記事は、LLVM Coding Standards 翻訳の3/4です。


前:言語、ライブラリ、および標準 | 目次 | 次:スタイルの問題


機械的なソースの問題

ソースコードのフォーマット

コメント

コメントは、可読性と保守性について重要な部分のひとつです。皆さんご存知の通り、コードにはコメントすべきです。コメントを書く場合、適切な句読点と大小文字の英文で書きます。コードがなにを行おうとしているのか、またなぜ行おうとしているのかを記載することに焦点を絞り、微細に どうやるか を書くことは避けてください。ここでは、文書に重要な事柄がいくつかあります:

ファイルのヘッダ

すべてのソースファイルには、ファイルの基本的な目的を説明するヘッダが必要です。ファイルにヘッダがない場合、ツリーにチェックインしてはいけません。標準のヘッダは次のようになります:

標準のファイルヘッダ
//===-- llvm/Instruction.h - Instruction class definition -------*- C++ -*-===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
///
/// 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で始まる一節は概要として使われます。追加情報は空白行で区切る必要があります。アルゴリズムが実装されているか、トリッキーなことをしている場合は、参照した発表論文が含まれるようにしてください。任意のメモやコードの注意すべき落とし穴も同様です。

クラスの概要

クラスは良いオブジェクト指向設計の基本的な一部です。このように、クラス定義は、クラスが何に使われ、どのように働くか説明するコメントブロックを持つべきです。自明でないすべてのクラスは doxygenコメントブロックを有することが期待されます。

メソッド情報

クラスのメソッド(およびグローバル関数)定義も適切に文書化する必要があります。ここでは、何をするかについての簡単なメモや境界での挙動の説明のみが、必要です(特に凝ったことをしていない場合)。理想は、コード自体を読まなくても、あなたのインターフェイスの使い方が理解できることです。

ここで触れるとよいことは、想定外の事態で何が起きるかです:メソッドはnullを返す?アボートする?ハードディスクをフォーマットする?

コメント書式

通常は、C++スタイルのコメントを用います(普通のコメントに//doxygenの文書化コメントに///)。スペースもとらず、タイプ数も少なく、入れ子での問題等もありません。ですが、Cスタイル(/* */)のコメントを用いたほうが良い場合もあります。

  1. Cコードを書くとき:Cのコードを書いていることが明らかであれば、Cスタイルのコメントを使ってください。
  2. Cソースファイルから#includeされるヘッダを書いている場合。
  3. Cスタイルのコメントしか受け付けないツール向けにソースファイルを書いている場合。

大きなブロックをコメントアウトするには、#if 0#endifを使います。これらはCスタイルのコメントよりも正しく適切に入れ子できます。

ドキュメンテーションコメントでのDoxygenの使用

\fileコマンドを使い、標準のファイルヘッダをファイルレベルのコメントに変換します。

すべてのパブリック・インタフェース(publicクラス、メンバと非メンバ関数)についての記述段落を含めます。API名をただ読み替えるだけにはしないでください。最初の文、または\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);
いい感じにすべてのDoxygenの機能を使うドキュメンテーションコメント
/// brief 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形式でなくても)入れられます。

コメントの先頭で関数名やクラス名を重複しないでください。関数やクラスが文書化されていることは人目に明らかであり、自動ドキュメント処理ツールは正しい宣言にコメントを紐づけられる程度に十分賢いです。

// In Something.h:
/// Something - An abstraction for some complicated thing.
class Something {
public:
  /// fooBar - Does foo and bar.
  void fooBar();
};

// In Something.cpp:
/// fooBar - Does foo and bar.
void Something::fooBar() { ... }
// In Something.h:
/// An abstraction for some complicated thing.
class Something {
public:
  /// Does foo and bar.
  void fooBar();
};

// In Something.cpp:
// Builds a B-tree in order to do foo.  See paper by...
void Something::fooBar() { ... }

追加のDoxygenの機能を使用する必要はありませんが、時にはそうするのは良い考えかもしれません。

考えてみましょう:

  • 関連する関数や型を含む小さな名前空間へのコメント追加。
  • 名前空間内で関連する関数を整理するための、トップレベルのグループの使用。
  • クラス内のメンバを整理するための、メンバグループおよびコメントの追加。
class Something {
  /// name Functions that do Foo.
  /// @{
  void fooBar();
  void fooBaz();
  /// @}
  ...
};

#includeの形式

ファイルヘッダのコメント(およびヘッダファイルの場合はインクルードガード)直後に、そのファイルに必要最低限の#includeを並べます。私たちは、これらの#includeがこの順に並んでことを好みます。

  1. メインモジュールヘッダ
  2. ローカル/プライベートヘッダ
  3. llvm/...
  4. システムの#include

各カテゴリは、フルパスで辞書順にソートする必要があります。

メインモジュールヘッダファイルは、.hファイルで定義されたインタフェースを実装する.cppファイルに適用されます。この#includeは、それがファイルシステムのどこにあるかに関わらず、最初にincludeされるべきです。.cppファイルが実装するインタフェースをファイル先頭でincludeすることにより、ヘッダの#includeに含まれない隠された依存関係が無いことを確認できます。また、.cppの実装するインタフェースがどこで定義されているかを示す一種のドキュメントにもなります。

ソースコードの幅

テキストの80桁に収まるようにあなたのコードを記述します。これは、コードを印刷したり、xterm上でサイズを変更しないで見たい人の助けになります。

長い答えは、開発者がよくあるディスプレイで複数ファイルを横に並べられるように、コードの幅にいくつかの制限が存在しなければならないということです。あなたが幅の制限を選ぼうとするなら、好きに決められますが、何かを基準に選びたいかもしれません。80桁の代わりに(例えば)90桁にすることは、大きな価値を追加しないでしょうし、コードの印刷にも害でしょう。また、他の多くのプロジェクトは80桁に標準化しているため、一部の人はすでに彼らのエディタをそう設定しています(90列のような他のものと喧嘩する)。

これは、コーディング標準で多くの論争を巻き起こす問題のひとつですが、議論の余地はありません。

タブの代わりにスペースを使う

全ての場合において、ソースファイルでタブよりもスペースを好みます。みんな好きな字下げ幅が異なり、好きなインデントの形式も異なりますが、これは別にかまいません。何が問題かというと、異なるエディタ/ビューアはタブを異なるタブストップで展開することです。これはあなたのコードをまったく読めないような形にしてしまう可能性があり、それに対処する価値はありません。

いつものようにゴールデンルールに従いましょう:あなたが修正し、それを拡張している場合は、既存のコードのスタイルに従ってください。あなたが4スペースのインデントを好きでも、2スペースインデントのコードの真ん中ではそうしないでください。また、ソースファイル全体のインデント修正もしないでください:それはまったく意味のない膨大な差分を生みます。

コードインデントの一貫

さて、プログラミングを始めた年にあなたは、インデントが重要であると言われていました。もしあなたがそれを信じて身につけていないのであれば、今がその時です。さあやりましょう。C++11の導入に際しては、一貫し、保守しやすく、ツールにも優しいフォーマットとインデントについて、いくつかの提案に値する新しいフォーマットの課題があります。

ラムダはコードブロックと同様に整形

複数行のラムダを整形する際は、コードブロックと同様に整形してください。もし文中に複数行のラムダしかなく、その後に式もない場合、コードブロックのための標準的なインデントである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;
});

このフォーマットの最良の利点を得るには、あなたが設計する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のようなツールで自動整形できます。

言語とコンパイラの問題

コンパイラ警告はエラーと同様に扱う

あなたのコードでコンパイラが警告を出す場合は、何かが間違っています--- 正確にキャストしていない、「疑わしい」生成、または何か合法的な誤りを犯しています。コンパイラの警告は出力の合法的なエラーをカバーし、困難な翻訳単位を扱うことができます。

すべてのコンパイラ上ですべての警告を防ぐことはできませんが、それは望ましくありません。代わりに、良い徹底した警告セットを提供する標準のコンパイラ(gccなど)を選択し、それに固執します。少なくともgccの場合には、若干のコードの構文を変更するだけで、エラーの誤検出を回避することが可能です。例えば、このようなコードを書くときに悩ましい警告が出ます:

if (V = getValue()) {
  ...
}

gccは、おそらく==演算子のタイプミスではないかと警告します。ほとんどの場合はそうでなく、偽のエラーを抑制したいです。この特定の問題を解決するに、私はコードをこのように書き換えます:

if ((V = getValue())) {
  ...
}

これでgccは黙ります。あなたを悩ます任意のgcc警告は、コードを適切に揉むことで修正できます。

移植可能なコードを書く

ほとんどの場合、それは可能であり、完全に移植可能なコードを書くための理由の範囲内です。ポータブルなコードを書くことが不可能な場合は、明確に定義された(そしてきちんと文書化された)インターフェイスの背後に隔離します。

実際には、これはあなたがホストコンパイラについて多くを想定してはならないことを意味します(そしてVisual Studioが最低基準となる傾向があります)。高度な機能を使用する場合、それらはシンプルな外部APIを持つライブラリの詳細実装であるべきで、libSystem内に埋め込まれることが望ましいです。

RTTIや例外を使わない

コードと実行ファイルのサイズを減らすために、LLVMはRTTI(例えばdynamic_cast<>)や例外を使いません。これら2つの言語機能は、一般的なC++の従量課金原則に反して、例外がコードで使われなかったり、RTTIがクラスで使われなかったとしても、実行ファイルの膨張を引き起こします。このため、私たちはコード全体でそれらを無効にします。

つまり、LLVMはRTTIを手で展開したisa<>、cast<>、そしてdyn_cast<>のようなテンプレートを広く用います。RTTIのこの形式は、任意のクラスにオプトインで追加することができます。また、これらはおおむねdynamic_cast<>よりも効率的です。

静的コンストラクタを使わない

静的コンストラクタとデストラクタ(例えば、そのタイプのコンストラクタまたはデストラクタを持つグローバル変数)はコードベースに追加されるべきではなく、可能な限り除かなければなりません。またソースファイル間での初期化順が未定義であるというよく知られた問題があり、静的コンストラクタの全体コンセプトは、大規模なアプリケーションにライブラリとしてリンクされるLLVMの一般的な使われかたと対立しています。

別のアプリケーションでJIT用にLLVMがリンクされた場合を考えてみましょう(perhaps for OpenGL, custom languages, shaders in movies, etc). 静的コンストラクタの設計に起因して、LLVMはその大きなアプリケーションにていつどこで使われるかに関わらず、アプリケーションの起動時に実行されるでしょう。これには2つの問題があります:

  • 静的コンストラクタの処理時間がアプリケーションの起動時間に影響します--- 特にGUIアプリケーションでは重要な時間です。
  • 静的コンストラクタにより、アプリが多くの余分なページメモリをディスクから引き出します。各.oファイル内のコンストラクタコードと少しのデータ。また、touched/dirtyページは低メモリのVMではもっと負担になります。

私たちは、追加のLLVMターゲットやアプリケーションのライブラリへのリンクがゼロコストであることを強く望みますが、静的コンストラクタはこの目標に反します。

とはいえ、LLVMは残念ながら静的コンストラクタを含んでいます。great projectによりLLVMからすべての静的コンストラクタが取り除かれた暁には、将来退行しないように-Wglobal-constructors警告フラグ(Clangビルドの場合)を有効にします。

classstructキーワードの使用について

C++では、classstructキーワードはほぼ同じ意味で使えます。唯一の違いはクラス宣言の場合です:classはデフォルトでメンバがprivateですが、structはpublicです。

残念ながら、一部ののコンパイラは規則に従わず、定義に用いられたのがclassstructかにより、異なるシンボルを生成します(MSVCなど)。これは、リンク時に問題となり得ます。

  • classまたはstructによる定義と宣言では、それぞれ同じキーワードを使う必要があります。
class Foo;

// Breaks mangling in MSVC.
struct Foo { int Data; };
  • 経験則として、structすべてのメンバがpublic宣言されている構造にのみ用いるべきです。
// Foo feels like a class... this is strange.
struct Foo {
private:
  int Data;
public:
  Foo() : Data(0) { }
  int getData() const { return Data; }
  void setData(int D) { Data = D; }
};

// Bar isn't POD, but it does look like a struct.
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 very deliberate, no braces.
std::fill(foo.begin(), foo.end(), Foo("name"));

// The pair is just 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のようなコンテナクラス内の型定義は抽象化されすぎている型の典型例でしょう。

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(); }

前:言語、ライブラリ、および標準 | 目次 | 次:スタイルの問題


5
2
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2