2023/11/10 14:20:一部のコードを修正しました.
この記事は アカンクリスマスアドベントカレンダー2023 7日目 の記事です.
はじめに
私は普段からC++という言語をよく使用しており,基本的なことは理解しているつもりでいます.しかし,全く知らなかった仕様によるバグで詰まることがよくあります.普段は知らなくても動くため気にもしておらず,突然知らない仕様で詰まる.プログラミングをしている方はこのような経験がよくあるのではないでしょうか.
本記事では,そんな多くの人が気にしないようなものとして,C++のsize_t
の仕様を取り上げ解説していこうと思います.
前置き
本記事内ではC++ 20を前提とします.また,断りがない限りコンパイラはGCC 13.2.0を仮定します.規格書はC++ 20のワーキングドラフトを参照しています.
最新の情報や公式の国際規格(有料)を知りたい方は規格書についてまとめて頂けているサイトがあるため,こちらを参考にしてください.
size_t
size_t
はC言語やC++で用いられる型の一つです.size_t
はその名の通りサイズを表すための型で,非負整数として扱われます.配列などのサイズを表現したり,for文のloop indexとして使われることが多いです.
#include <array> #include <iostream> constexpr size_t HOGE_SIZE = 5; int main() { std::array<int, HOGE_SIZE> ary{1, 2, 3, 4, 5}; for (size_t i = 0; i < ary.size(); ++i) { std::cout << ary[i] << std::endl; } }
size_t
の仕様
size_t
は非負整数として扱われはしますが,bit数は仕様として決定されていません.規格書には以下のように記載されています.十分な大きさであることは保証されていますが,具体的な大きさの記載はないです.つまり環境依存です.
The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object (7.6.2.4).
https://timsong-cpp.github.io/cppwp/n4861/support.types.layout#3
実際にGCCのコードを見ると,GCCの定義済みマクロである__SIZE_TYPE__
にtypedef
してあり,GCCに依存していることが分かります(該当部).
typedef __SIZE_TYPE__ size_t;
ちなみに,私の環境では__SIZE_TYPE__
はlong unsigned int
でした.long
も結局環境依存(参考)なので環境依存のたらい回しが凄いです...
$ gcc -dM -E -c -xc /dev/null | grep __SIZE_TYPE__ #define __SIZE_TYPE__ long unsigned int
size_t
とstd::size_t
ここまでsize_t
の話をしていましたが,実はsize_t
は2種類存在します.1つはC言語から使われるsize_t
,もう一つはC++のSTLに内包されるstd::size_t
です.2種類あることだけを知っていると使い分けが難しそうに見えますが,規格書に以下の記載があるため問題ないです.
For each type T from the C standard library, the types ::T and std::T are reserved to the implementation and, when defined, ::T shall be identical to std::T.
https://timsong-cpp.github.io/cppwp/n4861/extern.types#1
要するにsize_t
とstd::size_t
は全く同一のものを指すということです.そのためどちらを使っても問題ないです.
GCCでは以下のようにsize_t
はそのままstd::size_t
のことを指しています(該当部).
using std::size_t;
size_t
とsize_type
C++でarray
やvector
などのコンテナをfor文で走査する場合,次のようにsize()
というメンバ関数でサイズを取得すると思います.
#include <vector> int main() { std::vector<int> v{1, 2, 3, 4, 5}; for (std::size_t i = 0; i < v.size(); ++i) {} }
ここで,i < v.size()
に着目します.一見問題ないように見えますが,v.size()
は実はsize_type
という型で返るため,i
の型であるsize_t
と異なる型での比較が起こってしまいます.
ではsize_type
とはどういう型なのでしょうか?規格書には以下のように記載されています.
size_type can represent any non-negative value of difference_type
https://timsong-cpp.github.io/cppwp/n4861/container.requirements.general#tab:container.req
非負整数であることは保証されていますが,サイズはコンテナ依存で規定されていません.そのため,やはり前述したfor文による走査は環境依存で壊れうると言えます.
ただし,GCCでは基本的にはsize_t
とsize_type
は同一のものとしており,例えばvector
の実装に以下のような記述があります(該当部).
typedef size_t size_type;
GCCに限らずほとんどの場合でsize_t
とsize_type
は同じと言って良さそうなので,神経質でなければ前述のfor文も問題ないと思います.
size_t
を正しく使う
size_t
の仕様について理解したはずなので,正しい使い方を学んでいきましょう.
size_t
を使わない
これが一番です.使わなければ何も問題は起きないので仕様を理解する必要もありません.for文でindexを加算していくなんてやめて拡張for文を使いましょう.
#include <iostream> #include <vector> int main() { std::vector<int> v{1, 2, 3, 4, 5}; for (const auto& x : v) { std::cout << x << std::endl; } }
どうしてもindexが欲しい場合はrangesを使うと良いです.
#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> v{1, 2, 3, 4, 5}; // C++20 for (auto i : std::ranges::views::iota(0ull, v.size())) { std::cout << i << " " << v[i] << std::endl; } // C++23以降 for (const auto& [i, x] : std::ranges::zip_view(std::ranges::views::iota(0ull, v.size()), v)) { std::cout << i << " " << x << std::endl; } }
明示的に型を合わせる
どうしてもsize_t
を使いたい場合は,環境依存にならないように静的に型キャストしたり型を取得したりして明示的に合わせましょう.
#include <iostream> #include <type_traits> #include <vector> int main() { std::vector<int> v{1, 2, 3, 4, 5}; using T = std::common_type<size_t, decltype(v)::size_type>::type; for (T i = 0; i < static_cast<T>(v.size()); ++i) { std::cout << v[i] << std::endl; } for (decltype(v.size()) i = 0; i < v.size(); ++i) { std::cout << v[i] << std::endl; } }
なお,間違っても次のようなコードを書いてはいけません.size_t
(size_type
)はintと同じサイズではなく,多くの場合でintよりも大きいです.コンテナサイズも同様にintより大きくなりうるので,このコードではコンテナサイズがintで表現できるサイズを超えた場合に無限ループします.
#include <iostream> #include <vector> int main() { std::vector<int> v{1, 2, 3, 4, 5}; for (int i = 0; i < v.size(); ++i) { std::cout << v[i] << std::endl; } for (unsigned int i = 0; i < v.size(); ++i) { std::cout << v[i] << std::endl; } }
おわりに
Rustなら全部解決なのになぁ...