C++ コア ガイドライン:式とステートメントの規則

C++ コア ガイドラインには、式とステートメントを扱う非常に多くの規則があります。正確には、宣言、式、ステートメント、算術式に関する 50 以上のルールがあります。

一般的なルールと呼ばれる 2 つのルールについて言及するのを忘れています。ここにいます。

ES.1:標準ライブラリを他のライブラリより優先し、「手作りのコード」

要約すると、生のループを書く理由はありません。つまり、double のベクトルです:

int max = v.size(); // bad: verbose, purpose unstated
double sum = 0.0;
for (int i = 0; i < max; ++i)
 sum = sum + v[i];

STL の std::accumulate アルゴリズムを使用する必要があります。

auto sum = std::accumulate(begin(a), end(a), 0.0); // good

このルールは、CppCon 2013 での Sean Parent の次の言葉を思い出させます。

もっと直接的に言うと、生のループを書くと、STL のアルゴリズムを知らない可能性があります。

ES.2:言語機能を直接使用するよりも適切な抽象化を優先する

次の既視感。私の最後の C++ セミナーの 1 つで、strstream を読み書きするためのいくつかの非常に洗練された手作りの関数について、長い議論に続いてさらに長い分析を行いました。参加者はこれらの機能を維持する必要があり、1 週間後には何が起こっているのかわかりませんでした。

機能を理解できない主な障害は、機能が適切な抽象化に基づいていないことでした。

たとえば、std::istream を読み取るための手作り関数を比較します。

char** read1(istream& is, int maxelem, int maxstring, int* nread) // bad: verbose and incomplete
{
 auto res = new char*[maxelem];
 int elemcount = 0;
 while (is && elemcount < maxelem) {
 auto s = new char[maxstring];
 is.read(s, maxstring);
 res[elemcount++] = s;
 }
 nread = &elemcount;
 return res;
}

対照的に、次の関数はどのくらい簡単に使用できますか?

vector<string> read2(istream& is) // good
{
 vector<string> res;
 for (string s; is >> s;)
 res.push_back(s);
 return res;
}

正しい抽象化とは、多くの場合、関数 read1 のような所有権について考える必要がないことを意味します。これは関数 read2 には当てはまりません。 read1 の呼び出し元は結果の所有者であり、それを削除する必要があります。

宣言は、名前をスコープに導入します。正直、私は偏見があります。一方では、次の規則は非常に明白であるため、少しお借りしています。一方で、これらのルールを恒久的に破るコードベースをたくさん知っています。たとえば、元 Fortran プログラマーと話し合ったところ、次のように言われました:各変数は正確に 3 文字である必要があります。

とにかく、ルールを提示し続けます。なぜなら、適切な名前はおそらく、コードを読みやすく、理解しやすく、保守しやすく、拡張しやすくするための鍵となるからです ...

最初の 6 つのルールは次のとおりです。

ES.5:スコープを小さく保つ

スコープが小さい場合は、画面に表示して、何が起こっているかを把握できます。スコープが大きくなりすぎる場合は、コードをメソッドを持つ関数またはオブジェクトに構造化する必要があります。論理エンティティを識別し、リファクタリング プロセスでわかりやすい名前を使用します。その後、コードについて考えるのがずっと簡単になります。

ES.6:for ステートメントの初期化子と条件で名前を宣言する制限範囲

最初の C++ 標準以降、for ステートメントで変数を宣言できます。 C++17 以降、if または switch ステートメントで変数を宣言できます。

std::map<int,std::string> myMap;

if (auto result = myMap.insert(value); result.second){ // (1)
 useResult(result.first); 
 // ...
} 
else{
 // ...
} // result is automatically destroyed // (2)

変数 result (1) は、if ステートメントの if および else ブランチ内でのみ有効です。結果は外側のスコープを汚染せず、自動的に破棄されます (2)。これは、C++17 より前では実行できません。外側のスコープで結果を宣言する必要があります (3)。

std::map<int,std::string> myMap;
auto result = myMap.insert(value) // (3)
if (result.second){ 
 useResult(result.first); 
 // ...
} 
else{
 // ...
} 

ES.7:共通名とローカル名を短くする、一般的でない非ローカル名をより長く保持

このルールは奇妙に聞こえますが、私たちはすでに慣れています。変数に i または j という名前を付けるか、変数に T という名前を付けると、コードの意図がすぐに明らかになります。i と j はインデックスであり、T はテンプレートの型パラメーターです。

template<typename T> // good
void print(ostream& os, const vector<T>& v)
{
 for (int i = 0; i < v.size(); ++i)
 os << v[i] << '\n';
}

このルールの背後にはメタルールがあります。名前は一目瞭然である必要があります。短いコンテキストでは、変数が何を意味するかが一目でわかります。これは、より長いコンテキストで自動的に保持されるわけではありません。したがって、より長い名前を使用する必要があります。

ES.8:似たような名前を避ける

この例をためらうことなく読めますか?

if (readable(i1 + l1 + ol + o1 + o0 + ol + o1 + I0 + l0)) surprise();

正直なところ、数字の0と大文字のOによく悩まされます。使用するフォントによっては、かなり似ているように見えます。 2 年前、サーバーにログインするのにかなりの時間がかかりました。私の自動生成されたパスワードは文字 O でした。

ES.9:ALL_CAPS を避ける 名前

ALL_CAPS は一般的にマクロに使用されるため、ALL_CAPS を使用すると、マクロ置換が有効になる場合があります。次のプログラム スニペットには、少し驚きが含まれている可能性があります。

// somewhere in some header:
#define NE !=

// somewhere else in some other header:
enum Coord { N, NE, NW, S, SE, SW, E, W };

// somewhere third in some poor programmer's .cpp:
switch (direction) {
case N:
 // ...
case NE:
 // ...
// ...
}

ES.10:宣言ごとに 1 つの名前 (のみ) を宣言する

例を 2 つ挙げましょう。 2 つの問題を見つけましたか?

char* p, p2;
char a = 'a';
p = &a;
p2 = a; // (1)

int a = 7, b = 9, c, d = 10, e = 3; // (2)

p2 は単なる文字 (1) であり、c は初期化されていません (2)。

C++17 では、このルールに 1 つの例外があります:構造化バインディングです。

これで、ルール ES.6 のイニシャライザを使用して if ステートメントをよりクリーンで読みやすく書くことができます。

std::map<int,std::string> myMap;

if (auto [iter, succeeded] = myMap.insert(value); succedded){ // (1)
 useResult(iter); 
 // ...
} 
else{
 // ...
} // iter and succeeded are automatically destroyed // (2)

次は?

もちろん、宣言に関するルールについては、次の投稿で続けます。