Visual Studioデバッグテクニック
プログラマーの尾関です。
今回は知っていそうであまり知られていなさそうな (気がする) Visual Studioのデバッグテクニックを紹介したいと思います。
概要
今回紹介するデバックテクニックは以下のとおりです。
- 条件付きブレークポイント:指定した条件が成立したときだけプログラムの実行を停止させるブレークポイント
- トレースポイント:ブレークポイントに「アクション」を設定し、停止せずにログ出力だけ行う
- ウォッチウィンドウの活用:クイックウォッチ(Shift + F9)やウォッチウィンドウで複雑な式や変数の状態をリアルタイムで監視できます
- データブレークポイント:特定の変数やメモリアドレスの値が「変更されたとき」にだけプログラムの実行を停止させる機能
- 関数ブレークポイント:関数名を指定して、その関数が呼び出されたときだけ停止するブレークポイント
条件付きブレークポイント
例えばあるループ処理において、ループ変数が特定の値になった場合にブレークしたいことはよくあると思います。
そういったとき、通常は以下のように if文を書くことでブレークできます。
#include <iostream>
int main()
{
for(int i = 0; i < 100; i++) {
if(i == 50) { // i が 50のときに下の行にブレークポイントをおいてブレークします.
std::cout << "i is 50" << std::endl;
}
std::cout << i << std::endl;
}
}
ただ、例えばデバッグ実行中にブレークしたいと思ったときに、一度プログラムを停止して再コンパイルするのは面倒なことがあります。例えばビルド時間が長い、その場面にたどり着くまでに(再現するまでに)時間がかかる、などです。
そういった「再コンパイル・ビルド→再度実行」をしたくない場合に「条件付きブレークポイント」は便利です。
方法としては、まずいつものようにブレークポイントを配置します。

次にそのブレークポイントを右クリックして「条件」を選びます。

「条件」の項目に判定したい式を指定して「閉じる」をクリック。

するとブレークポイントの中心に「白い+」がついて条件ブレークが設定できました。

実行して確認してみます。指定した条件「i == 50」のときのみブレークするようになりました。

トレースポイント (アクション) の使い方
ブレークポイントには「アクション」という項目があります。
この項目にチェックを入れると、このブレークポイントにヒットしたときにメッセージを出力することができます。

なおこの指定をすると、ブレークは行わずに「出力ウィンドウ」にメッセージが出力されるのみとなります。

ウォッチウィンドウの活用
ブレークが行われると、ローカルウィンドウにローカル変数が表示されます。

ここからローカル変数や thisポインタ などを見ることができますが、例えば static 変数はローカル変数ではないので見ることができません。
例えば以下のようなコードです。
#include <iostream>
// static変数テスト.
static int s_hoge = 123;
int main()
{
std::cout << s_hoge << std::endl; // 最適化で消さないように参照します.
return 0;
}
ローカル変数以外の値を見る場合には「ウォッチウィンドウ」を開きます。
そしてウォッチしたい変数名を入力するとその値が見れるようになります。

このとき、変数名をダルブリックなどして選択し、ドラッグ&ドロップで登録することも可能です。

さらにウォッチウィンドウは「数式」を入れて値を評価することができます。
例えば「i + j」といった式を指定することも可能です。

この機能を応用して、例えば異なる文字コードの文字列を表示することも可能です。
#include <iostream>
int main()
{
// UTF-8の文字をcharに代入
const char* utf8Char = u8"テスト";
// UTF-16の文字を2byte変数の配列に格納
const short int* utf16Char = (short int*)u"文字";
return 0;
}
この記述だとローカルウィンドウでは値を見ることができません。

そこでウォッチウィンドウで以下のように変数をキャストします。
- (char8_t*)utf8Char
- (char16_t*)utf16Char
これによりウォッチウィンドウで文字の中身が確認できるようになります。

データブレークポイント
データブレークポイントは、特定の変数やメモリアドレスの値が「変更されたとき」にだけプログラムの実行を停止させる機能です。これは「変数の値が書き換わっているけれど、どこで書き換わっているのかがわからない」というときに便利です。
具体的なケースとしては、配列のオーバーランを起こしたり、ラムダ式でキャプチャした変数がいつの間にか nullptr になっている…といったことが現場ではよくあります。
#include <iostream>
struct Test {
int arr[3] = {};
int b = {};
};
int main()
{
Test t = Test();
t.b = 12;
t.arr[0] = 1;
t.arr[3] = 3;
std::cout << "Test.b = " << t.b << std::endl; // なぜか値が3になる
}
例えば上記のようなコードで Test.b がなぜか「3」なってしまうという問題です。
シンプルなコードなので、C言語のポインタの知識があれば原因はすぐにわかると思いますが、このような配列のアドレスは正しいけれども配列オーバーしていた、といったケースはまれに見ます。
この原因を突き止める一つの方法としてデータブレークポイントを使ってみます。
方法は「ブレーク時」に対象の変数を右クリックして「値が変更されたときに中断」を選びます。

するとブレークポイントにデータブレークポイントが追加されました。

そして”F5″ などで実行を再開すると、値が変更された「次の行」でブレークが発生します。

これで「値が書き換わっているけれど、どこで書き換わっているのかわからない…」といった問題に直面したときに、この方法が解決の糸口となる可能性があります。
関数ブレークポイント
関数ブレークポイントは、「指定した関数名に到達したタイミングでプログラムの実行を中断したい」場合に使います。通常の行ブレークポイントとは異なり、関数の実装場所が分からなくても、関数名だけでブレークポイントを設定できるのが特徴です。
あまり例はよくないのですが、以下の main関数内で、hoge1() の呼び出し時に「呼び出される側」にブレークポイントを置きたいとします。
#include <iostream>
static void hoge1()
{
std::cout << "hoge1" << std::endl;
}
static void piyo()
{
std::cout << "piyo" << std::endl;
}
int main()
{
piyo();
hoge1(); // これがどこで定義されているのかわからない
return 0;
}
ブレークポイントのウィンドウの「新規作成 > 関数のブレークポイント」をクリック。

ブレークポイントを置きたい関数名 (ここでは hoge1関数) を「文字列」で指定します。

すると、hoge1関数の呼び出し部分にブレークポイントが配置されました。

今回のケースではコードのすぐ上にあるので、わざわざ関数ブレークポイントを使う必要はないですが、呼び出し元に定義ジャンプせずに直接配置したい場合に便利です。
さらにクラス関数でも関数ブレークポイントは使えます。
#include <iostream>
class Hoge {
public:
void test() { std::cout << "test" << std::endl; }
};
int main()
{
Hoge hoge;
hoge.test();
return 0;
}
例えば、上記コードの Hoge::test() 関数にブレークポイントを置く場合…。

関数ブレークポイントから “Hoge::test” を入力します。
するとクラス関数にもブレークポイントを置くことができました。

おしまい
以上、Visual Studioのデバッグテクニックの紹介でした。
今回の記事が Visual Studioでの開発のデバッグに役立てれば幸いです。
おまけ:ウォッチしている値を書き換える・実行している行を移動する
記事を書いた後に気がついたのですが、知らない人に教えると驚かれる機能として「ウォッチしている値を書き換える」「実行している行を動かす方法」があります。
ローカルウィンドウ、ウォッチウィンドウ、どちらの変数でも直接値を書き換えることができます。

そして、もう1つが実行している行の移動。黄色い矢印をマウスでドラッグ&ドロップで移動できます。

ただ、この方法でおかしな位置に移動するとハングアップすることがあるので、その点は注意して使います。