C++
GCC
型変換

GCCの警告でC++のオーバーフローが出ない問題

More than 1 year has passed since last update.

この記事を書くきっかけ

ビット演算子でスマートに正の偶奇を判定するのコメントでnum%2==1が奇数判定としては負の数で不的確だという指摘があった、それは正しくGCCでは(-3)%2-1と評価される。
似たようプログラマーの勘違いによる誤動作は各型の表せる範囲をまたぐときに起こりえる。詳しくはC/C++はnull安全になる前に安全に差の絶対値を計算できるようになるべきではないかを見てもらう。
特にunsignedは境界が0とよく使う領域に近いので特に注意が必要である。
これらの記事での結論はコンパイラに警告を出させろだった、その結論に反論する気は毛頭ないがコンパイラはおせっかいな警告も出すので結局、警告を出していても気にしない環境が出来上がってしまう。
そこで0からプログラムを書きつつどのような警告を出すか見ていく。
gccで Wall & Wextra を使っても有効にならない警告、量にげんなりするがgccを使うなら一度は目を通しておくといいかも

はじめに

main.cc
#include <bits/stdc++.h>
int main(){ }

C++なのにヘッダーファイルに.hがついています。main関数に返り値がありません。ですがこれらはセーフです。main関数のみ返り値の省略が許されています。ヘッダーファイルの規定はありません。

g++ -Wall -Wextra main.cc

以降、autoを使いたいので-std=c++14をつけます。

Unsigendを試したいので

変数を宣言する。

main.cc
#include <bits/stdc++.h>
int main(){
  int num;
  unsigned unsigend_num;
}

はいアウト〜、[-Wunused-variable]使ってない変数は作るな!
じゃあ使えばいいんだろ。

main.cc
#include <bits/stdc++.h>
int main(){
  int num;
  unsigned unsigend_num;
  std::cout<<num*unsigend_num<<std::endl;
}

標準出力で出させます。これでもアウト、[-Wuninitialized]初期化されていませんとのこと。

main.cc
#include <bits/stdc++.h>
int main(){
  int num=0;
  unsigned unsigend_num=0;
  std::cout<<num*unsigend_num<<std::endl;
}

よし、警告が出なくなった!ん、...それで良いのか?
unsigendとintの計算は安全なのか?イヤ、多分ダメだろうとダメだろうということで試す。

2017/1014 追記

返り値として受け取ってるわけでもないのでどっちかといえばセーフの部類だろう。signedとunsignedの演算なのでsignedが返ってきているはずなのでそこまで危険ではないと思う。
データのオーバフローも考えられるがそれは型とか関係なく掛け算するなら常に存在する危険性である。

main.cc
#include <bits/stdc++.h>
int main(){
  int num=INT_MAX;
  unsigned unsigend_num=INT_MAX;
  std::cout<<num<<"*"<<unsigend_num<<"="<<num*unsigend_num<<std::endl;
}

INT_MAXとかは確かマクロ定義だったはずちょっと危険な香りがするが警告は出さなかった。
結果、

2147483647*2147483647=1

と訳のわからない出力をだす。
結局、いらん警告は出すが本当に欲しい警告は出してくれなかった、終了〜としてもいいがもうちょっと調べる。

2017/1014 追記

常識的は範囲で計算しましょうの一言で終わる話だった。

numeric_limitsを使う。

main.cc
#include <bits/stdc++.h>
int main(){
  int num=std::numeric_limits<int>::max();
  unsigned unsigend_num=std::numeric_limits<int>::max();
  std::cout<<num<<"*"<<unsigend_num<<"="<<num*unsigend_num<<std::endl;
}

INT_MAXがマクロ定義だったのでnumeric_limitsを使ってみる。unsignedintを突っ込んでるのはわざとだったんですが警告は出ませんでした。
修正

main.cc
#include <bits/stdc++.h>
int main(){
  int num=std::numeric_limits<int>::max();
  unsigned unsigend_num=std::numeric_limits<unsigned>::max();
  std::cout<<num<<"*"<<unsigend_num<<"="<<num*unsigend_num<<std::endl;
}
2147483647*4294967295=2147483649

結果、unsigned_numが倍になりました。負の部分がないのでこれが正解です。

unsignedに引き算

main.cc
#include <bits/stdc++.h>
int main(){
  unsigned unsigned_num=0;
  unsigned_num--;
  std::cout<<unsigned_num<<std::endl;
}

本題に行き着く前にこけてしまったわけですが一応試します。
unsignedに引き算をします。C++erならこんなコード書くなと怒られることうけあいですが警告は出しません。結果、4294967295unsignedで表せる最大値になります。

キャスト

main.cc
#include <bits/stdc++.h>

int main(){
  int num=0;
  unsigned unsigned_num=num;
  int num2=unsigned_num;
  std::cout<<num2<<std::endl;
}

警告を出しません。

2017/10/14 追記

main.cc
#include <bits/stdc++.h>

int main(){
  int num=0;
  unsigned unsigned_num=num;
  int num2=unsigned_num;
  std::cout<<num2<<std::endl;
}

これは表せる数字の範囲が結構違うので出して欲しかったですが出ませんでした、逆のキャストも出しませんでした。

比較

main.cc
#include <bits/stdc++.h>

int main(){
  int num=0;
  unsigned unsigned_num=num;
  std::cout<<(num<unsigned_num)<<std::endl;
}

やっと警告が出ました。[-Wsign-compare]、符号なしと符号付きは比較するなと。一応、実行0、値が同じなので比較はfalseで0になります。falseを0に変換してくれるあたりC++は親切ですね。falseで出したければstd::boolalphaをストリームに流し込みましょう。

googleのコーディング規約

@yumemtodoさんの記事あるようにgoogle様はfor (unsigned int i = foo.Length()-1; i >= 0; --i)これが無限ループするからunsignedを使うなと言っています。そもそもこんなトリッキーなコードを書くべきではないんですが
これを試すためにこんなコードをかきます。

main.cc
int main(){
  unsigned unsigned_num=10;
  for( unsigned i=unsigned_num; i>0; i-- ){
    std::cout<<i<<std::endl;
  }
}

警告は出ません。何故って>=でないからです。>=にするとcomparison of unsigned expression >= 0 is always true [-Wtype-limits]、まんまドンピシャの警告を出します。コンパイラ賢い!
ちなみに

main.cc
#include <bits/stdc++.h>

int main(){
  std::vector<double> foo={ 1, 2, 3 };
  for( int i=0; i<foo.size(); i++ ){
    std::cout<<foo[i]<<std::endl;
  }
}

[-Wsign-compare]を出します。vector.size()の返り値がunsignedだからです。for( unsigned i=0; i<foo.size(); i++ ) for( size_t i=0; i<foo.size(); i++ )なら警告は出ません、unsignedでも警告は出ませんが@lo48576さんの指摘通りsize_t型のほうがより安全です 。forループ内はunsignedsize_tを使ったほうが良さそうです。配列でループさせたいならrange-baised-forで良いですが...。

まとめ

gccはunsignedの比較関係は細かく警告を出してくれますがキャストや演算には無頓着です。特にunsignedの境界が0でよく使う数に近いので演算は細心の注意が必要です。別にsignedだからといって境界を気にしなくていいわけではありませんが普通の使い方をする限りまず見ることのない範囲だと思うのでunsignedよりは気にする必要性が少ないと思います。
と思っていましたが階乗$n!$の計算をしていた時に上限でオーバーフローしたことがあったことを思い出しました計算の性質を考えれば十分に考えられる範囲です。
ちょっと考えてみると計算のオーバーフローは実行時に値をチェックしないといけないのでコンパイル時の検出は難しいんでしょう。プログラマーはこういう問題があることを認識して適切な型を選ぶ必要があるんだと思います。例えば大きな値を扱いそうならlonglong long負の値が絶対無いならunsigned long long等。

Undefined Behavior Sanitizerを試す。

@lo48576さんの情報にあった。UB sanitizerを試してみました。リンクはgccのバージョンです。gcc4.9より新しい版で使えるようです。私の環境はgcc5.4です。

main.cc
#include <bits/stdc++.h>

int main(){
  int num=0;
  unsigned unsigned_num=num;

  unsigned_num--;
  std::cout<<unsigned_num<<std::endl;

  num=std::numeric_limits<int>::min();
  num--;// main.cc:11:8: runtime error: signed integer overflow: -2147483648 - 1 cannot be represented in type 'int'
}
g++ -Wall -Wextra -fsanitize=undefined main.cc

でコンパイル実行、エラーを出すのは8行目でした。
@SaitoAtsushi さんが指摘しているようにunsignedに対するオバーフローは定義されているので未定義動作では検出できないようです。