XcodeでC++デバッグ時にSTLにステップインする方法


XcodeでC++デバッグ時にSTLの関数にステップインするには

Xcode(というかlldb)で、C++のプロジェクトをデバッグするとき、通常ではSTLの関数にステップインできません。(ただし、後述するように、ステップインできるものもあります)

つまり、デバッグ時に

std::shared_ptr<MyClass> p = std::make_unique<MyClass>(a, b, c);

std::make_unique関数や、

std::function<void(int, int)> f = [](int n, int m) {
    std::cout << n + m << std::endl;
};

f(10, 20);

std::functionの関数呼び出し演算子の中にステップインしようとしても、自動的にステップオーバーされてしまい、呼び出し階層を潜っていくことができません。

MyClassのコンストラクタやfに設定した関数の中にブレークポイントを仕掛けておけば、そこで実行が止まるので、それを利用する手もあります。しかし、ステップインするときに予めブレークポイントを仕掛けておかないといけないのは不便です。また、std::functionにいろいろな関数を設定する状況では、どの関数が実際に呼ばれるのかは、ステップインしてみないと分からない場合もあります。

LLDBの設定を変更する

関数呼び出しを自動的にステップオーバーする挙動は、LLDBのtarget.process.thread.step-avoid-regexpというオプションによって制御されています。このオプションに正規表現のパターンを設定しておくと、そのパターンにマッチする関数へのステップインが無視されて、自動でステップオーバーされるようになります。

Xcodeでは、Shift-Command-CというショートカットでLLDBのコンソールが表示できるので、その画面上で、このオプションの設定値を変更できます。

LLDBコンソール画面
(lldb) settings show target.process.thread.step-avoid-regexp
target.process.thread.step-avoid-regexp (regex) = ^std::
(lldb) settings set target.process.thread.step-avoid-regexp ""

上は、LLDBのコンソール画面で、target.process.thread.step-avoid-regexpオプションを変更したときのログです。先頭に(LLDB)とついている行がコマンドを実行した行で、先頭に何もついていない行が、実行結果を表しています。

最初に、lldbsettings showコマンドによって、このパラメータの値を確認しています。Xcode起動時には、lldbのデフォルト値として、ここに^std::が設定されているため、既定でstd名前空間にある関数やクラスのメンバ関数がスキップされるようになっています。

次のsettings setコマンドでは、これに空文字列""を設定しています。これによって、どの関数もスキップせずにステップインできるようになります。(空文字列のときは特別に二重引用符を使用していますが、それ以外の文字列を指定するときは、二重引用符は必要ありません。空文字列を指定する時以外で二重引用符を付けると、それもパターンの一部と認識されてしまいます。)

ここには正規表現を記述できるので、以下のように独自のパターンを設定できます。ただし、先読み/戻り読みのような拡張した文法は使用できないようです。(最新のLLDBではわかりませんが)

  • ^(std::|boost::)
    • STLだけでなく、boostライブラリもスキップする。
  • ^std::(__1::)?([^_f]|f[^u]|fu[^n]|fun[^c])
    • STLをスキップするが、std::functionはスキップしない

これをもとの状態に戻すには、

settings set target.process.thread.step-avoid-regexp ^std::

のようにして、^std::を再度設定します。

このオプションの設定値は、デバッグ実行中に動的に変更できます。
そのため、特定の箇所をデバッグしている時にだけ手動で値を変更して、ステップインする必要のない箇所をスキップするように設定すると、効率的にデバッグができます。

また、~/.lldbinit~/.lldbinit-Xcodeというファイルを用意しておき、そのファイルに設定を記載しておけば、デバッガ起動時にその設定を自動で有効にできます。

.lldbinit-Xcode
settings set target.process.thread.step-avoid-regexp ^(std::|boost::)

注意点

一つ注意点として、関数テンプレートに対しては、正規表現とのマッチがうまくいかない場合があります。

上記のオプションに設定した正規表現とマッチを試行する関数の名前は、関数のマングリング(mangling)されたシンボル名をデマングル(demangle)したものになります。関数テンプレートのシンボル名には、通常の関数と異なり、戻り値の型の情報も含まれているため、それをdemangleした時には、関数名の前に戻り値の型が現れます。

関数宣言とそれに対応するマングリング名
namespace ns1 {
    int foo() { return 0; } // __ZN3ns13fooEv

    template<class T>
    int bar() { return 0; } // __ZN3ns13barIiEEiv
}
デマングルした結果
% c++filt __ZN3ns13fooEv  
ns1::foo()
% c++filt __ZN3ns13barIiEEiv
int ns1::bar<int>()

このように、関数テンプレートであるbar()は、戻り値の型の情報をシンボル名に含んでいるため、デマングルした文字列の先頭に、戻り値の型であるintが含まれるようになります。

このため、ns1名前空間にある関数を除外しようと思って、正規表現に^ns1::を指定しても、bar()関数はそのパターンにはマッチせず、ステップインできてしまう、ということになります(ステップインしてしまったならステップアウトすればいいだけの話ではありますが)。

冒頭で言及した、

通常ではSTLの関数にステップインできません。(ただし、後述するように、ステップインできるものもあります)

というのも、つまりこのことで、デフォルトで^std::が設定されているにもかかわらず、特定の関数(関数テンプレートであり、戻り値の型がstd::名前空間以外で定義された型になるもの)については、ステップインできる、ということになります。

参考サイト

補足

std::functionの関数呼び出し演算子にステップインできるようになっても、引数の適用とかでステップ数が多いので、std::functionに設定した関数に到達するまで何度もステップインし続ける必要があって、実は結構面倒です。
なので結局、std::functionに設定する関数に直接ブレークポイント仕掛けられるならそのほうが早いことが多いです。