C++ 軽量コーディングスタイル – 意識低い系コーディング規約への誘い


C++による開発をより楽にするためのコーディングスタイルやコーディング規約を紹介していく。C++を軽量言語的に扱うためのテクニック集だと思ってもらうとよい。中には実際の開発現場での利用が難しいスタイルや規約もあるかもしれない。注意点とともに紹介していく。

コーディングスタイル編

コーディング規約編

スポンサーリンク

コーディングスタイル編

プライベートメンバの命名をアンダースコアで始める

アンダースコアで始まるメンバ変数やメンバ関数を暗黙的にプライベートメンバとみなす。

struct Number {
  // 非公開メンバ変数
  double _value;
  // 非公開メンバ関数
  std::string _stringValue() { return std::to_string(_value); }
  
  // 公開メンバ関数
  double value()       { return _value; }
  double doubleValue() { return _value; }
  int    intValue()    { return _value; }
  void setValue(double value) { _value = value; }
};

シンボル名を見ただけで、それがプライベートなメンバであることが瞬時に理解できるようになる。そのため可読性や判読性の向上が見込める。また仮引数とメンバ変数の名称の重複が発生しないという利点も生まれる(Before: this->value = value, After: _value = value

double value()のような、ゲッターの接頭辞を省略したメソッド宣言も可能になる。他にもprivateキーワードによる明示的なプライベート指定を、後回しにできるという開発者都合の利点がある。

追記(2017-06-21)

なおアンダースコアで始まるシンボル名は、グローバル空間では予約済み識別子となるため注意したい。アンダースコア + 小文字で始まるシンボル名については、構造体宣言文の内部や関数内部であれば問題なく利用できる。

以下の定義は未定義の動作に繋がる。

typedef struct _Number /* 未定義動作 */ {
  double _Value;  // 未定義動作
  double _VALUE;  // 未定義動作
  double __value; // 未定義動作
  double value__; // 未定義動作
  double val__ue; // 未定義動作
} Number;

クラスをstructキーワードで定義する

// Before
class X {
 public:
  X(int v) : v(v) {}
 private:
  int v;
};

// After
struct X {
  X(int v) : _v(v) {}
  int _v;
};

メンバへのフルアクセスが可能になる。開発段階ではprivate:等のアクセス指定子も指定しないようにすると良い。そうするとデバッガー(LLDB, GDB)によるメンバ関数の動的呼び出しやメンバ変数の参照/書き換えが制限なく行えるようになる。

またその際には、先程紹介したアンダースコア命名規則を用いて、暫定的なプライベート指定を表現すると良い(int _v;)。公開メンバと非公開メンバの区別が可能になる。

他の外部ツールとの連携時にも制限が少なくなる。かゆいところに手が届く。

本スタイルにはアクセス周りの指定を後回しにできるという利点がある。アクセス制御はある程度テストや設計が固まった頃に行うとよい。本スタイルはあくまで開発の初期段階での利用に留める。

横幅をふんだんに使ったコードを書く

複数行のコードはやり過ぎない程度にワンライン化する。

// Before
switch (align) {
  case left:
    return "Left";
  case right:
    return "Right";
}

// After
switch (align) {
  case left:  return "Left";
  case right: return "Right";
}

マルチステートメントを活用する。

switch (align) {
  case left:  title = "<-"; toolTip = "左"; break;
  case right: title = "->"; toolTip = "右"; break;
}

メンバ関数をワンライン化する。メンバ間の空行も省略する。

// Before
struct String {
  const char* _s;
  
  auto begin() {
    return _s;
  }
  
  auto end() {
    return _s + strlen(_s);
  }
};

// After
struct String {
  const char* _s;
  
  auto begin() { return _s; }
  auto end()   { return _s + strlen(_s); }
};

関数定義も簡潔に記述する。

// Before
template<class T>
auto length(const T* s) {
  return strlen(s);
}
template<class T>
auto length(const T& s) {
  return s.size();
}

// After
template<class T> auto length(const T* s) { return strlen(s); }
template<class T> auto length(const T& s) { return s.size();  }

コードはできるだけコンパクトにまとめて書くようにする。そうすると、両コードの違いがはっきりと識別できるようになる。コードの規則性を意識する。縦方向に長いコードを避け、横方向を活かしたコードを書くよう意識する。

コード整列のススメ – 読みやすいコードを意識するプログラミング作法

戻り値型にautoを使う

C++14では戻り値型にautoを使うことができる(C++11では若干制限がある)。

struct String {
  std::string s;
  // Before
  std::string::iterator begin() { return s.begin(); }
  std::string::iterator end() { return s.end(); }
  // After
  auto begin() { return s.begin(); }
  auto end() { return s.end(); }
};

開発初期の段階では、面倒な型の明示をautoで簡略化するとよい。戻り値型はreturn文の内容から推論される。ただし、戻り値型が曖昧になるため、ドキュメント性は悪くなる。その場合はIDEや外部ツールを用いてautoを本来の型に置き換える(無ければそのうち誰かが作ってくれる)。

ちなみに、C++14/C++11の関数宣言文では、型名を後置することも可能となっている。

auto begin() -> std::string::iterator { return s.begin(); }
auto end()   -> std::string::iterator { return s.end(); }

複数ある関数名の表示位置が、縦方向に綺麗に揃うようになるため、ドキュメント性に優れたコードにもなる。意識低い系という本スタイルの趣旨からは外れるが、オススメしたいスタイルの一つでもある。

メンバ関数の定義をヘッダファイル側で行う

メンバ関数の定義は可能な限りクラス宣言文の内部で行う。実装ファイルを減らすことができる。ヘッダオンリーのライブラリを実現することも可能となる。

Before

// Range.h
struct Range {
  int start, end;
  bool equals(Range r);
  bool contains(int i);
};

// Range.cpp
bool Range::equals(Range r) {
  return start == r.start && end == r.end;
}
bool Range::contains(int i) {
  return start <= i && i < end;
}

After

// Range.h
struct Range {
  int start, end;
  
  bool equals(Range r) {
    return start == r.start && end == r.end;
  }
  bool contains(int i) {
    return start <= i && i < end;
  }
};

ただし、ヘッダオンリーのクラスファイルはインクルードファイルの隠蔽が行えなくなるという欠点がある。クラスの依存関係やロジックが複雑になるような場合には、従来通り実装ファイル側に処理を定義するようにする。またメンバ関数のコード行数やボリュームが多くなるような場合も同様に、実装ファイル側に定義を分離する。

構造体の変換コンストラクタは宣言しない

開発初期の段階では面倒な変換コンストラクタの宣言を行わないようにする。

// Before
struct Range {
  int start, end;
  Range(int start, int end) : start(start), end(end) {} // 変換コンストラクタ
};

Range a(2, 5);
Range b = Range(2, 5);

// After
struct Range {
  int start, end;
};

Range c{2, 5};
Range d = {2, 5};
Range e = Range{2, 5};

C++11では統一初期化記法({})による構造体/クラスの初期化が可能となっている。変換コンストラクタの定義なしに、メンバの初期化が行える。ちなみに同等の初期化記法はC言語でもオブジェクト = {初期化子並び};と言う形で用いることができる。

なお、初期化リストによる初期化は、メンバの初期化忘れに繋がる危険性があるため、データ構造が単純な構造体への利用や開発段階での利用に留めるようにするとよい。

ちなみに統一初期化記法利用後に変換コンストラクタを定義することも可能となっている。既存の初期化記法({2, 5})は変換コンストラクタ(Range(int start, int end))を呼び出すようになる。

開発初期の段階では、面倒な変換コンストラクタの宣言を後回しにし、デフォルト引数やメンバの順序入れ替え等が必要になった時に初めて、変換コンストラクタやその他のコンストラクタを定義するようにする。

メンバ変数の初期化を宣言時に行う

// Before
struct Shop {
  int price;
  Shop() : price(99) {} // デフォルトコンストラクタによる初期化
};

// After
struct Shop {
  int price = 99; // メンバ初期化子による初期化
};

C++11では、データメンバ向けの新たな初期化子が利用できる。これによって、デフォルトコンストラクタやコンストラクタ初期化子の利用を省略することが可能になる。

ただし、C++11では初期化リストによる初期化との混在ができないという問題がある。もっともC++14ではこの問題は解消されている。

struct X {
  int a;
  int b = 2;
} x{1}, y{11, 22}; // error: no matching constructor for initialization of 'X'
                   // note: candidate constructor (the implicit default constructor) not viable: requires 0 arguments, but 1 was provided

コーディング規約編

typedefではなくusingを使う

「意識高すぎ」という指摘を受けたため、以下の記事へ移動した。

C++ 重量コーディングスタイル - 意識高い系コーディング規約への導き

コンストラクター初期化子のフォーマット方法

コンストラクター初期化子における各メンバ初期化子の区切り文字(カンマ)は常に先頭に記述する。

struct X {
  int a, b, c;
  X()
    : a()
    , b()
    , c() {}
};

自動インデント時や、自動コード整形機能の影響を抑える効果がある。

struct X { // 良くない例
  int a, b, c;
  X()
    : a(),
    b(),
    c() {}
};

ちなみに、カンマが後置された初期化子並びは一般的な文との区別が曖昧になるという問題もある。

struct X {
  int a, b, c;
  X() :
    a(),
    b(),
    c() {
    d();
    e();
  }
};

カンマを前置することで、コンストラクター初期化子の式と一般的な式を区別することができる。

演算子オーバーライド時の空白は省略

「意識高すぎ」という指摘を受けたため、以下の記事へ移動した。

C++ 重量コーディングスタイル - 意識高い系コーディング規約への導き

template宣言文の改行は自由

どちらでも良い。どちらか一方に統一する必要もない。

// OK
template<class T>
void fn(T v) {}
// OK
template<class T> void fn(T v) {}

テンプレートクラスの宣言時とテンプレート関数の宣言時で両記法を区別するのもよい。

縦に長いコードよりも横に長いコードを良しとするのなら、後者の方法の利用がオススメである。横に長いコードはワードラップ有効時に途切れてしまうという問題があるが、テキストエディタ側でスマートラップやbreakindent等の機能を有効にすればよい。

// ワードラップ(OFF)
    template<class T> void function_name(type_name parameter_name) {}
// ワードラップ(ON)
    template<class T> void function_name(
type_name parameter_name) {}
// breakindent
    template<class T> void function_name(
    type_name parameter_name) {}
// スマートラップ
    template<class T> void function_name(
            type_name parameter_name) {}

template宣言文の空白は省略する

// Before
template <class T> 
// After
template<class T>

これは特殊化や型指定の際の構文を意識している。また文字数の削減もできる。

struct clazz<int>;
fn<int>(99);

// こう書く人はあまり見かけない
struct clazz <int>;
fn <int>(99);

template宣言文の改行省略時と指定時を区別するために空白を用いるのも良いかもしれない。またはテンプレートクラスとテンプレート関数の区別にも使える。

// 改行省略時は空白を省略
template<class T> void fn(T v) {}
// 改行指定時は空白で区別
template <class T>
void fn(T v) {}

広告

関連するオススメの記事