C++開発をスマートに行うためのナウでヤングなコーディングスタイルを紹介していく。C++を職業プログラマ的な目線で安全に扱うためのテクニック集だと思ってもらうとよい。
コーディングスタイル編
コーディング規約編
関連
コーディングスタイル編
マクロは使わない
マクロ置換やマクロ定数は定義しないように注意する。型検査の強力なテンプレート機能やインライン関数、const修飾子による定数宣言を用いるようにする。
// Before
#define HELLO "hello"
#define print(v) std::cout << v << std::endl;
#define each(a, b) for (auto&&_ : a) { b; }
// After
const char* HELLO = "hello";
template<class T> void print(const T& v) { std::cout << v << std::endl; }
template<class T, class F> void each(const T& a, F f) { for (auto&&e : a) f(e); }
オブジェクトは波括弧で初期化する
// Before
std::string s("foo");
// After
std::string s{"foo"};
std::string s("foo");
のような記述は関数宣言式や関数呼び出し式等の一般的な処理と混同しやすいという問題があるが、std::string s{"foo"};
であればこの曖昧さを回避できる。より可読性の高い明確なセマンティクスを実現できる。
一時変数はautoで宣言する
面倒な型名のタイプが不要になる。コードも簡潔になる。
std::string s = "abc";
// Before
std::string::iterator begin = s.begin();
std::string::iterator end = s.end();
// After
auto begin = s.begin();
auto end = s.end();
auto
という明確なキーワードが行の先頭に記述されるようになるため、変数宣言式の判別が容易になる。一貫性や直観性に優れた記法となる。
ヘルパー関数を使う
C++標準ライブラリが提供するテンプレートクラスの多くには、対になるヘルパー関数が用意されている。これでオブジェクトの生成を簡略化することができる。
// Before
std::unique_ptr<int> ptr(new int(9));
std::tuple<char, int> tup('C', 99);
// After
auto ptr = std::make_unique<int>(9);
auto tup = std::make_tuple('C', 99);
ヘルパー関数の利用によって、autoによる宣言式を活かすこともできる。特にstd::unique_ptr<int> ptr;
のように型名を先頭に書く記法は、変数名の表示位置が型名によって右側へと押しやられてしまい、読み取りづらいコードになってしまいがちだが、しかしautoによる宣言記法ならこれが軽減される。
また複数の宣言式を列挙する際に、それぞれの変数名の位置が綺麗に揃うようになるため、規則的なコードにもなりやすい。
// Before
char c = 'C';
int i = 99;
// After
auto c = char('C');
auto i = int(99);
ドキュメント的で読みやすいコードになる。
なおchar('C')
やint(99)
による型を明示した形での初期化は、若干意識が高すぎる気もする。リテラルを見ればその型の種類は自明であるから、過剰な明示は不要と考えることもできる。
auto c = 'C'; // char
auto i = 99; // int
auto l = 99L; // long
auto d = 0.0; // double
auto f = 0.0f; // float
ただ、以下のコードについては、両者の意味は異なるため、用途に合わせて慎重に扱っていきたい。
auto s = "s"; // const char*
auto s = std::string("s"); // std::string
ちなみにauto s = "s";
は型推論と変換コンストラクタの特性を活かしたコードとして威力を発揮する。
auto f = [](std::string s) {};
auto s = "s";
f(s);
明確さや厳格さ、意識の高さを重視する本スタイルの趣旨にはあまり合わない作法かもしれないが、現代プログラミングでは常識の作法となりつつある。
クラス名や関数名には明確な命名を行う
クラス名や関数名、定数名には略称を用いずに、明確なものを採用する。
// Before
bool eq();
size_t len();
class Ite {};
class Tok {};
// After
bool equal();
size_t length();
class Iterator {};
class Token {};
略称を用いた型はあくまで内部処理でのみ扱うようにする。usingやtypedefで型の別名を作る方法が有効である。
class Parser {
private:
using Tok = Token;
Tok pop();
void push(Tok);
public:
Token next();
Token peek();
std::vector<Token> tokenize();
};
特に外部に公開するクラスや定数名には略称を用いないように注意したい。
ただ、略称を用いないと、文字入力が大変になったり、コンパイルエラーのログやスタックトレースの一覧、入力補完機能のリストが読みづらくなる場合がある。実際に現場のプログラマはサジェストやデバッカ、スタックトレース、ログの情報量を減らすためにあえて意図的に短い略称を好んで用いることがある。しかしこの手の問題の多くはIDEや外部ツールの質を向上させることでどうにでもなる。いずれ誰かが解決する些細な問題である。
コーディング規約編
以前の記事 C++ 軽量コーディングスタイル – 意識低い系コーディング規約への誘い から意識高めの規約をこちらに移動した。typedefではなくusingを使う
古い時代のライブラリや標準ライブラリでは型の別名を付ける際にtypedef
が用いられていることが多いが、これからはusing
を使うようにする。直感的な記法でわかりやすい。
// Before
typedef std::string String;
typedef std::vector<std::string> Strings;
// After
using String = std::string;
using Strings = std::vector<std::string>;
また、少ない労力でコード整列が行える。Beforeの例では空白文字を過剰に入力しなければならない。
C言語との互換性や移植性を意識してtypedefを用いるのも良いが、クラスや構造体の内部では、読みやすさと機能性の観点からusingの利用を推奨したい。
演算子オーバーライド時の空白は省略
// Before
Number operator + (int rhs) {}
// After
Number operator+(int rhs) {}
これには明示的なoperator関数呼び出し時の記法と統一する意図がある。タイプ数も減る。また一般的に使われている関数宣言の構文(return_type function_name()
)との一貫性も意識している。
Number obj{1};
Number a = obj + 2;
Number b = obj.operator+(2); // この記法
全文検索時に宣言部の記法と呼び出し部の記法との区別が付かなくなる恐れはあるが、大した問題にはならない。少し面倒だがoperator+(
と.operator+(
で区別して検索すれば良い。
プログラミング言語の種類によっては、空白を用いたほうが可読性が高まる場合があるが、C++の場合はoperatorキーワードの存在そのものが可読性に寄与するため、空白による過剰な整形は不要であると考えられる。
fun + (lhs: Int, rhs: Int): Number; // 必要
Number operator+(int lhs, int rhs); // 不要
空白を用いる場合は、空白を演算子の先頭に寄せるようにすると良い。
Number operator () (int v); // Before
Number operator ()(int v); // After
この記法の利用は、キャスト演算子のオーバロード時(operator int();
)の記法との一貫性を保つことにも繋がる。
空白を用いる場合は、空白を演算子の末尾に寄せるようにすると良い。
Number operator () (int lhs); // NO
Number operator() (int lhs); // OK