potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C++&Visual Studio 便利なデバッグ変数情報の視覚化(natvis)と関数使用時の注意

Visual Studio 2022はC++でユーザー定義のデバッグ変数ウィンドウ視覚化に対応しています。この機能natvisでクラスのメンバー関数を使おうとして戸惑ったので共有します。

natvisの紹介

例えば次の2つのファイルmain.cpptest.natvisC++プロジェクトにあるとtest1だけ変数情報がカスタマイズされます。

main.cpp

class test1
{
public:
    int x;
};

class test2
{
public:
    int x;
};

int main()
{
    test1 t1;
    test2 t2;

    return 0;
}

test.natvis

<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> 
    <Type Name="test1">
        <DisplayString>test1 value {x}</DisplayString>
        <Expand>
            <Item Name="value">x</Item>
        </Expand>
    </Type>
</AutoVisualizer>

natvisは拡張子.natvisのファイルで、Visual Studio 2022であれば新しい項目の追加ダイアログの「Visual C++➡ユーティリティ➡デバッガー視覚化ファイル (.natvis)」からテンプレートを作成できます。Microsoft Learnには分かりやすい公式情報「Natvis フレームワークを使用してデバッガーで C++ オブジェクトのカスタム ビューを作成する」があり、SDKをインストールしていれば参考資料としてSTLのnatvisも確認できます。

STLのnatvis:C:\Program Files\Microsoft Visual Studio\2022\Preview\Common7\Packages\Debugger\Visualizers(Cドライブインストール時)

natvisはメンバー関数の情報も表示できます。次のようにnatvisで関数f1()を書けば結果が表示できます。

class test1
{
public:
    auto f1() { return 123; }
};

int main()
{
    test1 t1;

    return t1.f1();
}
<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> 
    <Type Name="test1">
        <DisplayString>test1 {f1()}</DisplayString>
        <Expand>
            <Item Name="[value]">f1()</Item>
        </Expand>
    </Type>
</AutoVisualizer>

うまく表示されない場合はローカル変数ウィンドウなどを右クリックして関数の評価を有効にします。

natvisで関数が消える場合と対処

便利なnatvisなのですが、急に適用されなくなり困りました。結論はコンパイラによる最適化で使わない関数が削除されたことです。

次のコードを実行するとこれまで視覚化されたtest1の表示がnatvis適用前になります。

class test1
{
public:
    auto f1() { return 123; }
    auto f2() { return 456; }
};

int main()
{
    test1 t1;

    return t1.f1();
}
<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> 
    <Type Name="test1">
        <DisplayString>test1 {f1()},{f2()}</DisplayString>
        <Expand>
            <Item Name="[value1]">f1()</Item>
            <Item Name="[value2]">f2()</Item>
        </Expand>
    </Type>
</AutoVisualizer>

調べた結果、natvisで参照している関数f2()コンパイラ最適化による削除が原因でした。解決策はf2を意図的に呼び出すか、natvisで省略可能に設定するか、プロジェクト設定の変更です。ただし、どの方法もクラスを使う側の対応が必要です。

class test1
{
public:
    auto f1() { return 123; }
    auto f2() { return 456; }
};

int main()
{
    test1 t1;

    auto x = t1.f1() + t1.f2();

    return x;
}

または

<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> 
    <Type Name="test1">
        <DisplayString>test1 {f1()},{f2()}</DisplayString>
        <Expand>
            <Item Name="[value1]">f1()</Item>
            <Item Name="[value2]" Optional="true">f2()</Item>
        </Expand>
    </Type>
</AutoVisualizer>

または

解決策

STLのnatvis(stl.natvis)では表示情報として内部変数が積極的に使われています。これは内部変数のクラス・構造体(使われた変数のクラス・構造体)は最適化で消えないからだと思います。メモリ使用量の考慮は必要ですが、手っ取り早い対応は以下のどれかかなと思います。

  1. 関数を使うnatvis定義にはOptional="true"を設定する。
  2. クラス側で表示情報を内部変数に持つ。
  3. デバッグ時は不使用関数を削除しない(STLの多用時はたぶん重くなります)。

C# クラスに属性でIIDを持たせる

C#ではカスタム属性でクラス自体にIIDを持たせられます。ただし静的メンバーより動作は遅く、Guidのような非標準型は属性定義時の引数に渡せないようです。

using System.Reflection;

Console.WriteLine(IIDAttribute.Of<ClassWithIPersistIID>()?.ToString("B") ?? "(未定義)");
Console.WriteLine(IIDAttribute.Of<ClassWithoutIID>()?.ToString("B") ?? "(未定義)");
//> {0000010c-0000-0000-c000-000000000046}
//> (未定義)

class IIDAttribute : Attribute
{
    public readonly Guid Value;
    public IIDAttribute(string iid)
    {
        Value = Guid.ParseExact(iid, "D");
    }

    // コードが冗長なのでメソッドを用意します。
    public static Guid? Of<T>()
    {
        return typeof(T).GetCustomAttribute<IIDAttribute>()?.IID;
    }
}

// IID_IPersist
[IID("0000010c-0000-0000-C000-000000000046")]
class ClassWithIPersistIID
{
}

class ClassWithoutIID
{
}

属性クラスに属性を持たせることで適用対象も指定できます。

// クラスのみに適用可能
// 複数指定は拒否
// 派生クラスやオーバーライドへの継承は拒否
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
class IIDAttribute : Attribute
{
    ...
}

他の使い方は公式ドキュメントや実際の属性のソースコードを参照ください。

C# HRESULTパターン、例外パターン、Nullableパターンのどれを使うかで悩む

C#で自分用のクラスライブラリを作るとき、最中にもエラー対処パターンで悩みます。COM開発で使えるデザインパターンはおそらくHRESULTパターン、例外パターン、Nullableパターン(適当に名付けました)なのですが、一長一短です。

  • HRESULTパターン
    • エラーを軽く素早く具体的に伝えられます。3つの中でC環境でも使えるのはこれだけです。ただし、毎回HRESULTをチェックするのでC#のプロパティとは親和性が低く、outと組み合わせてもコードが一行長くなり可読性が少々犠牲になります。一方、昨今はoutと組み合わせればRAIIも対応できる強みもあり、他のパターンを使う場合もP/Invokeで多用します。
  • 例外パターン
    • C++でも使われてきた安定の方法です。プロパティとの親和性も高く、想定外の動作は即座に中断されるのでバグに気づきやすいです。また、行番号などが記載されるのでデバッグも容易です。弱点は例外発生時のコストが他よりも大きく、特定のエラーコードが想定される場合もtry構文が必要で冗長になることでしょうか。そこだけ他のパターンにしても良いですが、一貫性は崩れます。一方、次のNullableパターンを使う場合もメモリ確保エラーなどの想定外のミスではお世話になります。
  • Nullableパターン
    • C++でもC#でもここ数年で本格導入されつつあるパターンだと思います。C++ならstd::optional (C++20)やstd::expected (C++23予定?)、C#ならNullable<T>T?を使います。従来はポインタでしか表現できなかったnullptr or nullを標準ライブラリの機能で値型にも提供するパターンです。メモリや速度のコストは少々増えますが、C++なら三項演算子や今後導入されるモナドC#ならパターンマッチングと極めて相性が良いです。ただし、C#ではnullptrが素通りしてしまう$"{...}"のような場合がしばしばあってバグの原因となりやすい印象があります。また、現状のC#ではC++std::expectedのようなメモリ消費の考慮された標準機能がないので具体的なエラーを把握しにくくなります。

WILのように複数のパターンを網羅してしまう(WILはHRESULT、例外、フェイルファスト)のもありだと思いますが、最適化でコードが消えない気がするC#ではすべてを準備するのはよろしくない気がします(最適化されていたらすみません)。ひとまずNullableパターンで落ち着こうと思いますが、まだまだ悩みながらになりそうです。

C# ジェネリック関数でEnum型を整数へ変換する

ジェネリック関数ではenum型は通常の方法((int)...)で整数へ変換できません。 Convert.ToInt32Convert.ToUInt32等を使えばジェネリック関数でもEnum型を整数へ変換できます。

using System;

Console.WriteLine(f(Enum1.A));

string f<T>(T t) where T : Enum
{
    // コメント解除すると以下のエラーが発生します。
    // エラー CS0030  型 'T' を 'uint' に変換できません
    //return $"{t} ({(uint)t})";

    return $"{t} ({Convert.ToInt32(t)})";

}

enum Enum1
{
    A = 0,
    B
}

日本語版Google検索でいくら検索しても非ジェネリック環境で(int)...するコードばかりヒットしました。SEOが優秀な某サイト群です。 専門的な情報は英語版Google検索で英単語を並べる方が良いみたいです。

HTML&JavaScript コメント要素を抜き出す

DOM中のコメント要素はnodeTypeNode.COMMENT_NODEのノードとして扱われます。なので親要素のchildNodesから取得できます。

// .parent要素からコメント要素を取得する。
parentNode = document.querySelector(".parent")
comments = Array.from(parentNode.childNodes).filter(node => node.nodeType == Node.COMMENT_NODE)
print(comments)

コメントは解析中も無視されると思っていたので取得できることに驚きました。

参考というかほぼ原文は以下です。