myoukakuのブログ

C++でゲームエンジンを作っていきます。

空のstd::vectorのdataの値

空のstd::vectorに対してdata()を呼び出した時、任意の値を返しても良いことになっている。

つまり

  • nullptr かもしれない
  • 有効なアドレスを指したポインタかもしれない

なぜかというと [data(),data() + size()) が有効な範囲を表すと定義されており、size()が0の場合はdata()の値が何であれこれを満たすから。

参考: c++ - What should std::vector::data() return if the vector is empty? - Stack Overflow

data()を使うときは、その前にempty()かどうかチェックしたほうがいい場合が多いだろう。

if (!v.empty())
    do_something(v.data());

Boost.Serialization で std::queue をシリアライズしようとするとエラーになった

現象

Boost.Serializationでstd::queueをシリアライズ/デシリアライズしようとするとビルドエラーになる場合があります。 具体的には以下のコードがエラーになります。

#include <boost/serialization/queue.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <sstream>

int main(void)
{
    std::stringstream ss;
    std::queue<int> q;
    boost::archive::text_oarchive oa(ss);
    oa << boost::serialization::make_nvp("q", q);  // (1)
    
    return 0;
}

(1)の行でエラーになります。 エラーメッセージは

error C2780: 'void boost::archive::save(Archive &,T &)' : 2 引数が必要です - 3 が設定されます。

他のアーカイブ(xml_oarchive,binary_oarchive)でも同様です。

また、text_iarchive(xml_iarchive,binary_iarchive)でデシリアライズしようとするとload関数で同様のエラーが出ます。

原因

queue.hppに定義されているserialize関数からは3つの引数を取るsave,load関数が呼び出されていますが、それが定義されていないのが原因のようです。

対処法

Boost.Serializationのqueueのテストを見てみると、queue.hpp だけでなく deque.hpp もインクルードしていました。 試しにdeque.hppもインクルードしてみたところビルドエラーは出なくなりました。 deque.hppの中を見てみると3引数版のsave,load関数が定義されており、これでビルドが通るようになったのだと思います。

結論

queueをシリアライズするときは<boost/serialization/deque.hpp>もインクルードする。 これが仕様なのかバグなのかはよくわかりませんでした。

静的コード解析

Doom 3」「Quake 3: Arena」「Wolfenstein: Enemy Territory」を、静的コード解析ツールである、CppCheckとPVS-Studioに掛けて結果を比較した記事。

Cppcheck and PVS-Studio compared

PVS-Studioの宣伝記事だと思うので、CppCheckとの比較はさておき、検出されたバグの内容がなかなか興味深かったのでまとめる。

sizeofに関連するバグ

現象
  • sizeof(型)とするべきところをsizeof(ポインタ)としてしまう
  • 定数 とするべきところを sizeof(定数) としてしまう
  • sizeof(*ポインタ) とするべきところを sizeof(&ポインタ) としてしまう

上の記事のなかではこれが最も多かった。memsetと一緒に現れることが多いのも特徴。 memset(ptr, 0, sizeof(*ptr)) とすべきところを memset(ptr, 0, sizeof(ptr)) としてしまっても、たいていは0にセットされるバイト数が少ないだけなので気づきにくいバグになる。 memcpyやmemcmpと一緒に現れることも考えられる。

「ポインタに関連するバグ」であると見ることもできる。

対策
  • memset、memcpy、memcmpを使わない

sizeofの部分は注意深く書くしかないと思うが、

  • 構造体を memset で初期化する
  • 構造体を memcpy でコピーする
  • 構造体を memcmp で比較する

のがこのバグの原因となっているし、やめるべきだと思う。

とするべきだろう。

単なるメモリブロックとして使っている場合は、std::vector を使うことができる。

メモリ確保と解放の不一致

現象
  • malloc して free していない
  • new[ ] で確保したのに delete で解放している

他にも

  • new して delete していない
  • new で確保したのに delete[ ] で解放している
  • new で確保したのに free で解放している
  • malloc で確保したのに delete で解放している

などのパターンも考えられる。

大きなくくりで言えば、「fopenしてfcloseしていない」も同じパターンだと見ることができる。

対策
  • スマートポインタを使う
  • std::vector を使う
  • malloc を使わずに new を使う

たいていは shared_ptr、unique_ptr、vector を適切に使えば解決するだろう。 スマートポインタはデフォルトでdeleteによって解放するので、メモリを確保するのにmallocを使わないようにしたほうがいいと思う。

基本的に、コード中に delete が出てこないようにするのが目標である。

コピペミス

現象

コピペして一部編集するときに直しそこねたことが疑われるパターン

  • if (...) {A} else {A}
  • if (A) {...} else if (A) {...}
  • 無駄な変数への代入
  • 変数を間違えて再利用している
  • for のループ変数を間違えている

など、一見して不可解で意図のわからない記述の原因となることが多い。 「変数に代入した直後に再度代入」や「then節とelse節が同じ」など、不具合は起こらないが全く意味のない記述となることもある。

また、その他の全てのバグの原因となっている可能性もある。

対策

コピペを減らすためには

  • 関数化する
  • template を使う
  • マクロを使う(どうしてもという場合)

などが考えられる。他にも構造体やクラスを使うことによってコピペを避けることもできるだろう。

コピペが原因のバグを完全になくすことは難しいかもしれない。コピペをした時にそれを自覚して、何か避ける方法がないか考える癖を付ける必要があるだろう。

printf系関数に関連するバグ

現象
  • printfでフォーマット文字列と渡す引数の不一致
  • sprintfの出力と入力に同じバッファが指定されている

など。未定義動作なので、運が良ければ停止するが、さもなければこっそりメモリを破壊して動き続けたりする。

printf系の関数は

  • 仕様が複雑
  • コンパイル時のチェックが少ない
  • 実装によって挙動が違う

などもあって鬼門である。

対策

printf系の関数の使用をさけるには、std::stream や boost::format を使うことが考えられる。

しかし、std::streamはイマイチ使いづらいし、Boostを全てのプロジェクトで使えるわけではないので悩ましい。

sprintfは std::string と std::to_string の組み合わせで避けられる場合もある。

演算子の優先順位に関連するバグ

現象
  • if (!(flag & CONSTANT)) とするべきところを if (!flag & CONSTANT) としている
  • (*ptr)++とするべきところを*ptr++としている

など。他にも数えきれないほどのパターンが考えられる。 複雑な条件式に含まれていると見落としがちである。

対策

優先順位をはっきりさせるために、丸括弧を多用する。優先順位が高くてカッコが必要ないときでも、カッコをつけるようにしたほうがいいだろう。

配列の範囲外へのアクセス

現象
  • tbl[sizeof(tbl)] = 0

圧倒的に、「配列の最後の要素+1」にアクセスしていることが多い。 環境によっては実行時に検出してくれることもある。

対策

生の配列よりstd::arrayやstd::vectorを使う。 単に全要素にアクセスするときはRange-Based For を使う。

enumに関連するバグ

現象
  • 別々のenum同士の比較
  • switch と case で違うenumを使っている

など。コンパイラで警告を出してくれることが多いと思うが、いったんintにキャストしていたりするとそのチェックもできない。

対策

enum よりも enum class を使う。 enum を整数型にキャストしない。

空のディレクトリを全て削除する

git-svnを使っていると、ディレクトリを消してGit上では見えなくなっても、SVNでチェックアウトすると空のディレクトリが残っています。dcommitするときに--rmdirオプションをつけていればいいのですが、もうdcommitしてしまった場合はSVN側から消すしかありません。

一つ一つ消すのは面倒なのでコマンドを使って空ディレクトリを全て削除します。 以下のサイトを参考にしました。

バッチファイル小技集:空フォルダをすべて削除する | 独学ツクールブログ

ソースファイルのエンコードと文字列リテラルのエンコード

結構勘違いしている人が多いし私も最近まで知らなかったのですが、C/C++のソースファイルのエンコードと文字列リテラルエンコードは同じではありません。おそらく処理系依存だと思います。

VisualStudioの場合、ソースファイルのエンコードに関係なく文字列リテラルエンコードSJISになります。ワイド文字列リテラルUTF-16です。

次のようにすると

const char s[] = "あ";
for (auto c : s)
{
    std::printf("0x%02x\n", (unsigned char)c);
}
0x82
0xa0
0x00

と表示されることで確認できます。

git svn rebase で unable to remap

ある日 git svn rebase すると次のようなエラーメッセージが出るようになりました。

C:\Program Files (x86)\Git\bin\perl.exe: *** unable to remap C:\Program Files (x86)\Git\lib\perl5\site_perl\5.8.8\msys\auto\SVN_Delta_Delta.dll to same address as parent -- 0x6E7500000 [main] perl.exe" 6228 sync_with_child: child 4280(0x2F4) died before initialization with status code 0x1191 [main] perl.exe" 6228 sync_with_child: *** child state child loading dlls

このエラーは前にも出たことがあって 以下のページ

http://stackoverflow.com/questions/21051874/git-svn-fetch-rebase-error-unable-to-remap-msys-ssl-0-9-8-dll-to-same-address-a

を参考に

rebase -b 0x64000000 bin/libsvn_repos-1-0.dll
rebase -b 0x64200000 bin/libneon-25.dll

してなおしたんですが、今回は追加で

rebase -b 0x64400000 "C:\Program Files (x86)\Git\lib\perl5\site_perl\5.8.8\msys\auto\SVN\_Delta\_Delta.dll"
rebase -b 0x64600000 bin/libsvn_wc-1-0.dll

としたらなおりました。