unsigned and size_t are hard. | Chandler@Berlin

Chandler@Berlin

ベルリン在住

これは計算機言語の細かい話なので,興味のない方は飛ばしたほうがいいでしょう.

C++ には unsigned という修飾子がある.これは符号無しの意味で,unsigned int のように使う.たとえば配列の index は負の値は普通意味がないので,この型を使ったりする.bit array の領域として使うには問題がないが,1 bit 欲しさにこれを int を unsigned int にするのは通常良くない考えである.(*) 特に implicit conversion が入ってくるとややこしい.計算結果が underflow しても符号無しというのは数としては直感的ではないと思うからである.

たとえば,unsigned int の -1 は 32bit マシンでは 4294967295 になることが多い.これは多くの計算機の実装がそうなるためである.このような計算機の内部表現を知っている人が書いたコードでも,ある日32bitで動いていたコードが64bit 環境に移った途端,動かなくなることがある.たとえば,size_t とunsigned int を交換可能なように書いているコードが,-1 を illegal な値として使う,ということをした場合である.

次の例が 32 bit で動いても 64 bit では動かないことがあるコードの例である.
---
#include <iostream>
#include <vector>

void foo(std::vector< int > & vec, size_t idx){
if(idx == size_t(-1)){
std::cout << "Illegal index" << std::endl;
return;
}
std::cout << "OK! accessing a vector with idx = " << idx << std::endl;
// vec[idx] = ...
}

int main()
{
std::vector< int > vec;
unsigned int idx = -1; // illegal index
foo(vec, idx);

unsigned int uint_minus_1(-1);
size_t size_t_minus_1(-1);
size_t size_t_casted = static_cast< size_t >(uint_minus_1);

std::cout << "(unsigned int)(-1) = " << uint_minus_1 << std::endl;
std::cout << "size_t(-1) = " << size_t_minus_1 << std::endl;
std::cout << "size_t(-1) (casted) = " << size_t_casted << std::endl;
}
---

結果は以下のようになる (64bit Linux)

---
nvlp[16]bash % ./unsigned_fail
OK! accessing a vector with idx = 4294967295
(unsigned int)(-1) = 4294967295
size_t(-1) = 18446744073709551615
size_t(-1) (casted) = 4294967295
---

まず,unsigned のものに -1 という signed の値を渡すこと自体に問題がある.また,このコードの問題は implicit な型の変換が行なわれていることにもある.C++ では unsigned に -1 を代入すると,型によって値が異なる.これはとても難しいことだと思う.最近の compiler はこれに warning を出してくれることがあるが,この warning は実は本当のバグかもしれないので注意している.unsigned is evil という友人がいた.私もできるだけ避けるようにしている.

計算機アーキテクチャを学ぶと,-1 と 4294967295 は等しい,ということが自然に思えてしまう.しかし最近私はできるだけ,それは実はおかしい,と思うようにしている.この例のように,その方がポータブルなコードが書けるからである.

(*) Bjarne Stroustrup, C++ Programming Language 3rd Ed. Section 4.4, paragraph 2. p.73