C++ コア ガイドライン:ステートメントと演算に関する規則

今日は残りの文のルールと算術ルールについて書きます。算術規則に従わない場合、未定義の動作が発生する可能性があります。

ステートメントには 4 つのルールが残されています。

  • ES.84:名前のないローカル変数を宣言しない (試みない)
  • ES.85:空のステートメントを見えるようにする
  • ES.86:生の for ループの本体内でループ制御変数を変更しない
  • ES.87:冗長な == を追加しないでください または != 条件に

最初のルールは非常に明白です。

ES.84:ローカル変数を宣言しない (しようとしない)名前のない変数

名前なしでローカル変数を宣言しても効果はありません。最後のセミコロンで、変数は範囲外になります。

void f()
{
 lock<mutex>{mx}; // Bad
 // critical region
}

通常、オプティマイザーは、プログラムの観察可能な動作を変更しない場合、一時的な作成を削除できます。これがいわゆる as-if ルールです。置くのは逆です。コンストラクターがプログラムのグローバル状態を変更するなどの観察可能な動作を行う場合、オプティマイザーは一時的な作成を削除できません。

ES.85:空のステートメントを表示する

正直なところ、このルールの理由がわかりません。なぜ空のステートメントを書きたいのですか?私にとっては、どちらの例も悪いものです。

for (i = 0; i < max; ++i); // BAD: the empty statement is easily overlooked
v[i] = f(v[i]);

for (auto x : v) { // better
 // nothing
}
v[i] = f(v[i]);

ES.86:本体内でループ制御変数を変更しないようにする生の for ループ

Ok。これは 2 つの観点から見れば、非常に悪い習慣です。まず、生のループを記述することを避け、標準テンプレート ライブラリのアルゴリズムを使用する必要があります。次に、for ループ内で制御変数を変更しないでください。これが悪い習慣です。

for (int i = 0; i < 10; ++i) {
 //
 if (/* something */) ++i; // BAD
 //
}

bool skip = false;
for (int i = 0; i < 10; ++i) {
 if (skip) { skip = false; continue; }
 //
 if (/* something */) skip = true; // Better: using two variable for two concepts.
 //
}

特に 2 番目の for ループについて推論するのが難しいのは、これらが 2 つのネストされた依存ループの内部にあるということです。

ES.87:冗長な == または != 条件に

私は有罪です。プロの C++ 開発者としての最初の数年間、条件で冗長な ==または !=をよく使用していました。もちろん、これはその間に変更されました。

// p is not a nullptr
if (p) { ... } // good
if (p != nullptr) { ... } // redundant 

// p is a nullptr
if (!p) { ... } // good
if (p == 0) { ... } // redundant 

for (string s; cin >> s;) // the istream operator returns bool
v.push_back(s);

これらは、ステートメントのルールでした。算術規則を続けましょう。これが最初の 7 つです。

  • ES.100:符号付き演算と符号なし演算を混在させないでください
  • ES.101:ビット操作には符号なし型を使用
  • ES.102:演算には符号付き型を使用
  • ES.103:オーバーフローしない
  • ES.104:アンダーフローしない
  • ES.105:ゼロで割らないでください
  • ES.106:unsigned を使用して負の値を回避しようとしないでください

正直なところ、これらのルールに追加することはほとんどありません。完全性 (および重要性) のために、ルールを簡単に説明します。

ES.100:符号付き演算と符号なし演算を混在させないでください

符号付き演算と符号なし演算を混在させると、期待した結果が得られません。

#include <iostream>

int main(){

 int x = -3;
 unsigned int y = 7;

 std::cout << x - y << std::endl; // 4294967286
 std::cout << x + y << std::endl; // 4
 std::cout << x * y << std::endl; // 4294967275
 std::cout << x / y << std::endl; // 613566756
 
}

GCC、Clang、および Microsoft コンパイラは同じ結果を生成しました。

ES.101:ビット操作に符号なし型を使用する

ルールの理由は非常に簡単です。符号付き型に対するビット演算は実装定義です。

ES.102:演算に符号付き型を使用する

まず、符号付きの型で算術演算を行う必要があります。次に、符号付き演算と符号なし演算を混在させてはなりません。そうでない場合、結果に驚くかもしれません。

#include <iostream>

template<typename T, typename T2>
T subtract(T x, T2 y){
 return x - y;
}

int main(){
 
 int s = 5;
 unsigned int us = 5;
 std::cout << subtract(s, 7) << '\n'; // -2
 std::cout << subtract(us, 7u) << '\n'; // 4294967294
 std::cout << subtract(s, 7u) << '\n'; // -2
 std::cout << subtract(us, 7) << '\n'; // 4294967294
 std::cout << subtract(s, us + 2) << '\n'; // -2
 std::cout << subtract(us, s + 2) << '\n'; // 4294967294

 
}

ES.103:オーバーフローせず、ES.104:ドンアンダーフローしない

両方のルールを組み合わせてみましょう。オーバーフローまたはアンダーフローの影響は同じです:メモリの破損と未定義の動作です。 int 配列で簡単なテストを作成しましょう。次のプログラムはどのくらい実行されますか?

// overUnderflow.cpp

#include <cstddef>
#include <iostream>

int main(){
 
 int a[0];
 int n{};

 while (true){
 if (!(n % 100)){
 std::cout << "a[" << n << "] = " << a[n] << ", a[" << -n << "] = " << a[-n] << "\n";
 }
 a[n] = n;
 a[-n] = -n;
 ++n;
 }
 
}

長い邪魔。プログラムは、100 番目の配列エントリを std::cout に書き込みます。

ES.105:ゼロで除算しない

クラッシュさせたい場合は、ゼロで除算する必要があります。論理式では、ゼロによるダイビングで問題ない場合があります。

bool res = false and (1/0);

式 (1/0) の結果は、全体の結果には必要ないため、評価されません。この手法は短絡評価と呼ばれ、遅延評価の特殊なケースです。

ES.106:を使用して負の値を回避しようとしないでくださいunsigned

負の値を避けたい場合は、unsigned 型を使用しないでください。結果は深刻になる可能性があります。算術演算の動作が変更され、符号付き/符号なし算術演算を含むエラーが発生する可能性があります。

以下は、符号付き/符号なしの算術演算を混在させたガイドラインの 2 つの例です。

unsigned int u1 = -2; // Valid: the value of u1 is 4294967294
int i1 = -2;
unsigned int u2 = i1; // Valid: the value of u2 is 4294967294
int i2 = u2; // Valid: the value of i2 is -2


unsigned area(unsigned height, unsigned width) { return height*width; } 
// ...
int height;
cin >> height;
auto a = area(height, 2); // if the input is -2 a becomes 4294967292

ガイドラインが述べているように、興味深い関係があります。 unsigned int に -1 を代入すると、最大の unsigned int になります。

次に、より興味深いケースに進みます。算術の動作は、符号付きと符号なしの型で異なります。

簡単なプログラムから始めましょう。

// modulo.cpp

#include <cstddef>
#include <iostream>

int main(){
 
 std::cout << std::endl;
 
 unsigned int max{100000}; 
 unsigned short x{0}; // (2)
 std::size_t count{0};
 while (x < max && count < 20){
 std::cout << x << " "; 
 x += 10000; // (1)
 ++count;
 }
 
 std::cout << "\n\n";
}

このプログラムの重要な点は、x inline (1) への連続した加算がオーバーフローをトリガーせず、x の値の範囲が終了した場合にモジュロ演算をトリガーすることです。その理由は、x が unsigned short (2) 型であるためです。

// overflow.cpp

#include <cstddef>
#include <iostream>

int main(){
 
 std::cout << std::endl;
 
 int max{100000}; 
 short x{0}; // (2)
 std::size_t count{0};
 while (x < max && count < 20){
 std::cout << x << " ";
 x += 10000; // (1)
 ++count;
 }
 
 std::cout << "\n\n";
}

x (2) が符号付きの型になるように、プログラム modulo.cpp に小さな変更を加えました。同じ追加がオーバーフローを引き起こすようになりました.

スクリーンショットでキーポイントを赤い丸でマークしました。

ここで、非常に疑問な点があります。オーバーフローを検出するにはどうすればよいですか?結構簡単。誤った代入 x +=1000; を置き換えます。中括弧を使用した式:x ={x + 1000};。違いは、コンパイラが縮小変換をチェックするため、オーバーフローを検出することです。 GCC からの出力は次のとおりです。

確かに、式 (x +=1000) と (x ={x + 1000}) は、パフォーマンスの観点からは同じではありません。 2 番目のものは、x + 1000 の一時的なものを作成できます。しかし、この場合、オプティマイザーは素晴らしい仕事をし、両方の式はフードの下で同じでした。

次は?

算数のルールはほぼ完成しました。これは、次の投稿で、パフォーマンスへのルールとともに旅を続けることを意味します.