はじめに
Rustの素晴らしさを啓もうする記事でC++の恐ろしさ(未定義動作)が露呈してしまっていたので、C++でどうチェックすればいいのか解説します。
未定義動作
Let's encryptのバグを再現したというC++のコードですが、これはスコープが切れてしまっているループ変数のアドレスを使用しているので未定義動作です。
# include <iostream>
# include <vector>
int main() {
std::vector<int*> out;
for (int i = 0; i < 3; i++) {
out.push_back(&i);
}
std::cout << out[0] << std::endl;
std::cout << out[1] << std::endl;
std::cout << out[2] << std::endl;
std::cout << *out[0] << std::endl;
std::cout << *out[1] << std::endl;
std::cout << *out[2] << std::endl;
return 0;
}
Visual Studio Lifetime Profile
Visual Studio 2019には上記のようなオブジェクトの生存期間に関するバグを警告してくれるVisual Studio 2019 Lifetime Profileという機能が付いています。この機能を使用して上記のコードを解析してみます。
デフォルトではオフなので機能を使用するためにプロジェクトのプロパティで以下の設定を行います。
Lifetime Profileで上記のコードを分析すると、std::coutをしている問題の行で2種類の警告(C26486とC26489)が表示されるようになります。
C24686は不正なポインタを関数に渡しているという警告です。上記のようにベクタに保存されているポインタはループ変数iへのポインタでiの生存期間が終わっているので不正なポインタになってしまっています。そのため不正なポインタout[0]、out[1]、out[2]を関数operator<<へ渡しているのでこの警告が表示されます。
2つ目のC26489は不正なポインタをdereferenceしようとしているという警告です。C24686と同じ理由でout[0]は不正なポインタのため、それをdeference(*out[0])しようとしているという警告です。
# include <iostream>
# include <vector>
int main() {
std::vector<int*> out;
for (int i = 0; i < 3; i++) {
out.push_back(&i);
}
std::cout << out[0] << std::endl; // warning C26486: Don't pass a pointer that may be invalid to a function.
std::cout << out[1] << std::endl;
std::cout << out[2] << std::endl;
std::cout << *out[0] << std::endl; // warning C26489: Don't dereference a pointer that may be invalid
std::cout << *out[1] << std::endl;
std::cout << *out[2] << std::endl;
return 0;
}
最後に
一度コンテナに保存されたポインタの生存期間に関する解析をしてくれるVisual Studio Lifetime Profileは素晴らしいですね!
普通にC++を書いていると生ポインタを扱うことは少ないかと思いますが、新しいツールやライブラリを使用してより安全なC++を書きましょう!

