C++ コア ガイドライン:宣言に関するその他のルール

この投稿で、宣言のルールを完成させます。宣言の残りの規則は特に洗練されたものではありませんが、高いコード品質にとって重要です。

はじめましょう。詳細に入る前の最初の概要は次のとおりです。

  • ES.25:オブジェクト 01 を宣言する または 18 後でその値を変更したくない場合
  • ES.26:2 つの無関係な目的で変数を使用しない
  • ES.27:29 を使用 または 34 スタック上の配列用
  • ES.28:特に 42 の複雑な初期化にはラムダを使用します 変数
  • ES.30:プログラムのテキスト操作にマクロを使用しない
  • ES.31:定数または「関数」にマクロを使用しない
  • ES.32:51 を使用 すべてのマクロ名
  • ES.33:マクロを使用する必要がある場合は、一意の名前を付けてください
  • ES.34:(C スタイルの) 可変個引数関数を定義しない

Python には、Zen of Python (Tim Peters) からの格言があります。「明示的は暗黙的よりも優れています」。これは、良いコードを書くための Python の一種のメタルールです。このメタルールは、特に C++ コア ガイドラインの次の 2 つのルールに当てはまります。

ES.25:オブジェクト 60 を宣言する または 74 後でその値を変更したくない場合

可能であれば、変数宣言に const または constexpr を使用する必要があるのはなぜですか?私にはたくさんの正当な理由があります:

  • あなたは自分の意思を表明します。
  • 変数が誤って変更されることはありません。
  • const または constexpr 変数は、定義上、スレッドセーフです。
    • const:変数がスレッドセーフな方法で初期化されることを保証する必要があります。
    • constexpr:C++ ランタイムは、変数がスレッドセーフな方法で初期化されることを保証します。

ES.26:2 つの無関係な目的で変数を使用しない

この種のコードは好きですか?

void use()
{
 int i;
 for (i = 0; i < 20; ++i) { /* ... */ }
 for (i = 0; i < 200; ++i) { /* ... */ } // bad: i recycled
}

ないことを願っています。 i の宣言を for ループに入れれば問題ありません。 i は for a ループの有効期間にバインドされます。

void use()
{
 for (int i = 0; i < 20; ++i) { /* ... */ }
 for (int i = 0; i < 200; ++i) { /* ... */ } 
}

C++17 では、if または switch ステートメントで i を宣言できます:C++17 - この言語の新機能は?

ES.27:<を使用コード>83 または 98 スタック上の配列用

10年前、スタック上に可変長配列を作るのはISO C++だと思っていました。

const int n = 7;
int m = 9;

void f()
{
 int a1[n];
 int a2[m]; // error: not ISO C++
 // ...
}

違う!

最初のケースでは std::array を使用し、2 番目のケースではガイドライン サポート ライブラリ (GSL) の gsl::stack_array を使用できます。

const int n = 7;
int m = 9;

void f()
{
 std::array<int, n> b1;
 gsl::stack_array<int> b2(m);
 // ...
}

C-array の代わりに std::array を、C-array の代わりに gsl::array を使用する必要があるのはなぜですか?

std::array は、C 配列とは対照的にその長さを知っており、関数パラメーターとしてポインターに減衰しません。次の関数を使用して、間違った長さ n の配列をコピーするのはどれほど簡単ですか:

void copy_n(const T* p, T* q, int n); // copy from [p:p+n) to [q:q+n)

int a2[m] などの可変長配列は、任意のコードを実行したり、スタックを使い果たしたりする可能性があるため、セキュリティ上のリスクがあります。

ES.28:特に 102 変数

セミナーで時々、次のような質問を耳にします。このルールは答えを与えます。複雑な初期化を入れることができます。このインプレース呼び出しは、変数が const になる必要がある場合に非常に役立ちます。

初期化後に変数を変更したくない場合は、以前の規則 R.25 に従って const にする必要があります。罰金。しかし、変数の初期化は、より多くのステップで構成される場合があります。したがって、const でなくてもかまいません。

こちらをご覧ください。次の例のウィジェット x は、初期化後に const にする必要があります。初期化中に数回変更されるため、const にすることはできません。

widget x; // should be const, but:
for (auto i = 2; i <= N; ++i) { // this could be some
 x += some_obj.do_something_with(i); // arbitrarily long code
} // needed to initialize x
// from here, x should be const, but we can't say so in code in this style

ここで、ラムダ関数が助けになります。初期化をラムダ関数に入れ、参照によって環境をキャプチャし、インプレースで呼び出されたラムダ関数で const 変数を初期化します。

const widget x = [&]{
 widget val; // widget has a default constructor
 for (auto i = 2; i <= N; ++i) { // this could be some
 val += some_obj.do_something_with(i); // arbitrarily long code
 } // needed to initialize x
 return val;
}();

確かに、ラムダ関数をその場で呼び出すのは少し奇妙に見えますが、概念的な観点からは、私はそれが好きです。初期化全体を関数本体に入れます。

ES.30、ES.31、ES.32 およびES.33

次の 4 つのルールだけをマクロに言い換えます。プログラムのテスト操作や、定数や関数にマクロを使用しないでください。それらを使用する必要がある場合は、ALL_CAPS で一意の名前を使用してください。

ES.34:(C スタイルの) 可変個引数関数を定義しない

右! (C スタイルの) 可変個引数関数を定義しないでください。 C++11 からは可変個引数のテンプレートがあり、C++17 からは折り畳み式があります。必要なのはこれだけです。

おそらくかなり頻繁に (C スタイルの) 可変引数関数 printf を使用したことでしょう。 printf は、フォーマット文字列と任意の数の引数を受け入れ、それぞれの引数を表示します。正しい書式指定子を使用しない場合、または引数の数が正しくない場合、print の呼び出しは未定義の動作をします。

可変個引数テンプレートを使用すると、型安全な printf 関数を実装できます。これは、cppreference.com に基づく簡易バージョンの printf です。

// myPrintf.cpp

#include <iostream>
 
void myPrintf(const char* format){ // (1)
 std::cout << format;
}
 
template<typename T, typename... Targs> // (2)
void myPrintf(const char* format, T value, Targs... Fargs) 
{
 for ( ; *format != '\0'; format++ ) {
 if ( *format == '%' ) {
 std::cout << value; // (3)
 myPrintf(format+1, Fargs...); // (4)
 return;
 }
 std::cout << *format;
 }
}
 
int main(){
 myPrintf("% world% %\n","Hello",'!',123); // Hello world! 123
}

myPrintf は、任意の数の引数を受け入れることができます。任意が 0 を意味する場合、最初のオーバーロード (1) が使用されます。任意が 0 より大きい場合、2 番目のオーバーロード (2) が使用されます。関数テンプレート (2) は非常に興味深いものです。任意の数の引数を受け入れることができますが、その数は 0 より大きい必要があります。最初の引数は値にバインドされ、std::cout (3) に書き込まれます。残りの引数は (4) で再帰呼び出しを行うために使用されます。この再帰呼び出しは、1 つ少ない引数を受け入れる別の関数テンプレート myPrintf を作成します。この再帰はゼロになります。この場合、関数 myPrintf (1) が境界条件として機能します。

すべての出力が std::cout によって処理されるため、myPrintf はタイプ セーフです。この簡略化された実装では、%d、%f、5.5f などのフォーマット文字列を処理できません。

次は?

表現について書くことはたくさんあります。 C++ コア ガイドラインには、約 25 の規則があります。したがって、次の投稿では表現について扱います。