C++による開発をより楽にするためのコーディングスタイルやコーディング規約を紹介していく。C++を軽量言語的に扱うためのテクニック集だと思ってもらうとよい。中には実際の開発現場での利用が難しいスタイルや規約もあるかもしれない。注意点とともに紹介していく。
コーディングスタイル編
- プライベートメンバの命名をアンダースコアで始める(推奨度:★★★☆☆)
- クラスをstructキーワードで定義する(推奨度:★★★☆☆)
- 横幅をふんだんに使ったコードを書く(推奨度:★★★★☆)
- 戻り値型にautoを使う(推奨度:★★★☆☆)
- メンバ関数の定義をヘッダファイル側で行う(推奨度:★★★★☆)
- 構造体の変換コンストラクタは宣言しない(推奨度:★★☆☆☆)
- メンバ変数の初期化を宣言時に行う(推奨度:★★☆☆☆)
コーディング規約編
- typedefではなくusingを使う(推奨度:★★★★★)
- コンストラクター初期化子のフォーマット方法(推奨度:★★★★★)
- 演算子オーバーライド時の空白は省略(推奨度:★★★★☆)
- typenameではなくclassを使う(推奨度:★☆☆☆☆)
- template宣言文の改行は自由(推奨度:★☆☆☆☆)
- template宣言文の空白は省略する(推奨度:★☆☆☆☆)
コーディングスタイル編
プライベートメンバの命名をアンダースコアで始める
アンダースコアで始まるメンバ変数やメンバ関数を暗黙的にプライベートメンバとみなす。
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
キーワードによる明示的なプライベート指定を、後回しにできるという開発者都合の利点がある。
他にもvalue_
という形で、アンダースコアを後置するスタイルも一般的となっている(Windows系のエンジニアがよく好む)。オープンソース系のプロダクトではm_value
という形でメンバ変数を明示するものも多くある。
アンダースコアを含む変数や識別子について
なおアンダースコアで始まるシンボル名は、グローバル空間では予約済み識別子となるため注意したい。アンダースコア + 小文字
で始まるシンボル名については、構造体宣言文の内部や関数内であれば問題なく利用できる。
以下の定義例は、新たな予約語や処理系定義の予約語との名前衝突、マクロ置換の影響を受ける危険性があるため注意したい。
typedef struct _Number /* NG */ {
double _Value; // NG
double _VALUE; // NG
double __value; // NG
double value__; // NG
double val__ue; // NG
} 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++ 重量コーディングスタイル - 意識高い系コーディング規約への導き
typenameではなくclassを使う
template仮引数宣言時におけるtypenameキーワードとclassキーワードは、いずれも区別されないため、本来であればどちらを用いても構わないが、ここでは文字数が少ないclassの利用を推奨する。
// Before
template<typename T> struct Number { T value; };
// After
template<class T> struct Number { T value; };
依存型名の型を取得する際に利用するtypename修飾子との区別も可能となる。
typename std::string::value_type c = char('9');
どうしてもclassとtypenameを区別したいような場合には、基本型とクラス型で両キーワードを使い分けるようにするとよい。
// template<typename T> struct Number { T value; };
template<class Number, typename T> void set(Number& n, T v) { n.value = v; }
// auto v = get(Number<int>());
template<template<typename T> class Number, typename T> int get(Number<T> n) { return n.value; }
ちなみにC++14ではテンプレートテンプレート仮引数でtypenameキーワードを利用することができない。
// エラーまたは警告: Template template parameter using 'typename' is a C++1z extension
template<template<class U> typename T, class U> void f(const T<U>& v) {}
// OK
template<template<class U> class T, class U> void f(const T<U>& v) {}
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) {}