C++erですがCOMに翻弄されています: 再入との戦い

# C++ Advent Calender

この記事は[C++のカレンダー | Advent Calendar 2023 - Qiita](https://qiita.com/advent-calendar/2023/cxx) の21日目の記事です。

- 20日目: [Try to make a try !](https://zenn.dev/wx257osn2/articles/try_to_make_a_try-saiudhleghbaowlb) by @wx257osn2
- 22日目: [C++ コンパイル時「出力」で画像ファイル生成 #C++ - Qiita](https://qiita.com/Raclamusi/items/fd9c5b52c514a6420e2c) by @Raclamusi

# はじめに

2021年に新卒で今の会社に入社して以来、ずっととある一つの製品の開発部門に所属していて、その中のWindowsチームというところにいます。

~~WindowsチームなのにiOSの要件が来たり、サーバーEoLの要件が来たりとなんだか特殊部隊感があるのですが、きっと気の所為です。~~

さて、ここまで入社以来ずっと本格的な理解から逃げ続けてきた存在があります。それがCOMです。

ところがついにこの秋から冬にかけての要件ではVisual Studioのバージョンアップを行うのに伴って、コアレイヤーに手を入れることが必要となり、COMとの戦いが幕を開けたのでした。

# COMとは

https://ja.wikipedia.org/wiki/Component_Object_Model

Component Object Modelの略です。著しくgooglabilityが低くて困りますね！

COMの説明の前にC++に立ち返って考えてみます。C++には仮想関数があります。

```cpp
#include <iostream>
#include <memory>
class IDisposable {
public:
    virtual void dispose() = 0;
};
class IExecutable {
public:
    virtual void Execute() = 0;
};
class Foo : public IDisposable, public IExecutable {
public:
    virtual void dispose() override { std::cout << "disposed" << std::endl; }
    virtual void Execute() override { std::cout << "executed" << std::endl; }
    virtual ~Foo() = default;
};
int main()
{
    std::shared_ptr<IDisposable> d = std::make_shared<Foo>();
    // do something
    d->dispose(); // => disposed
    const auto e = std::dynamic_pointer_cast<IExecutable>(d);
    if (!e) return 1;
    e->Execute(); // => executed
    const auto d2 = std::dynamic_pointer_cast<IDisposable>(e);
    if (!d2) return 1;
    d2->dispose(); // => disposed
}
```

https://wandbox.org/permlink/01kJgkE2iranM2GY

さて、この仮想関数がどのようにして実装されるかというと、多くの処理系で、vtableを用います。

https://pabloariasal.github.io/2017/06/10/understanding-virtual-tables/

https://qiita.com/4_mio_11/items/aa71f18b24ab55e4cb3d

上の例に示したように、基底クラス間の間でもdynamic_castを用いることでキャストできます。`IDisposable`な`d`に問い合わせて`IExecutable`を要求し`e`を得て、さらに`e`に問い合わせて`IDisposable`な`d2`を得られているのがわかると思います。

COMでは純粋仮想関数で表されるインターフェースをより明確にクラスから分離した上で、vtableを一般化します。COMでは各オブジェクトが最低でも`IUnknown`というインターフェースを実装しています。

```cpp
interface IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) = 0;
    virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
    virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
};
```

`dynamic_cast`に変わって`QueryInterface`を導入し、任意のインターフェスでのポインタをオブジェクトに要求するようになります。
また上の例ではオブジェクトの寿命を`std::shared_ptr`に管理させましたが、COMでは`AddRef`/`Release`を呼び出して対応します。また`QueryInterface`を呼び出すと新たなポインタを得るために参照カウントを1増やします。

# ATL

インターフェースと実装を明確に切り離したのはいいのですが、COMオブジェクトを全部自力で実装するのはわりかし大変であることが知られています。そこで、古くからATLを用いてCOMオブジェクトを作ることが行われてきました。

`ATL::CComPolyObject`とか`ATL::CComObject`なんかを利用して実装を行います。

# 私達のプログラムの概形

私達のプログラムはWindows Serviceとして作成されています。Windows Serviceなので、ユーザーがWindowsにログオンするよりも前から動きます。しかもその実行権限はやや特殊です。

いっぽうでWindows Serviceからユーザーに何かを通知したい、あるいはUIを提供したい、という時、Windows Serviceから直接はそういったことはできません。ユーザーセッションで行う必要があります。

ユーザーセッションとはなにか、簡単に知る方法として次のコマンドがあります。

```
PS C:\Users\yumetodo> query session
 セッション名      ユーザー名               ID  状態    種類        デバイス
 services                                    0  Disc
>console           yumetodo                  1  Active
 7a78855482a04...                        65536  Listen
 rdp-tcp                                 65537  Listen
 ```

これは実機でWindowsにログオンしている状態での結果です。session id: 1のconsoleセッションがいるのがわかると思います。

```
PS C:\Windows\system32> query session
 セッション名      ユーザー名               ID  状態    種類        デバイス
 services                                    0  Disc
                   tarou.yamada              3  Disc
                   ichiro.suzuki             5  Disc
 console                                    10  Conn
>rdp-tcp#0         yumetodo                 13  Active
 rdp-tcp#1         Administrator            40  Active
 rdp-tcp                                 65536  Listen
```

これはWindows Serverで複数人がRDPしている状態での結果です。session id: 13のrdp-tcp#0とsession id: 40のrdp-tcp#1がいるのがわかると思います。

すなわちユーザーセッションとは1つではありません。そこで、各ユーザーセッションごとに、プロセスを作って対処しています。

具体的には、sessin idとclsidからそれぞれセッションモニカとクラスモニカを作り、それをBindToObjectを用いてClassFactoryを生成し、起点となるCOMオブジェクトを各ユーザーセッションごとに生成しています。この時同時にプロセスも作られます。雰囲気としてはこんなコードです。

```cpp
IUnknownPtr CreateInstanceInSession(DWORD sessionId, REFCLSID clsid)
{
	return CreateInstance( BindToObject( SessionMoniker( sessionId ) / ClassMoniker( clsid ) ), NULL );
}
```

ユーザーセッションはユーザーのログオンやログアウト、RDPの切断など様々な要因で変化します。そのため、WTS_SESSION_xxx系イベントや関係しそうな様々なイベントを契機として、次のMSDNをまねて適当なIIDでQueryInterfaceして反応を探るということをしています。

[Checking the status of a COM server - ReuvenLax - Site Home - MSDN Blogs](https://web.archive.org/web/20151016061936/http://blogs.msdn.com:80/b/reuvenlax/archive/2005/11/08/490565.aspx)

```cpp
/**
@brief リモートのオブジェクトとの通信が切断されていないことの確認を行う。

戻り値がいずれであれ、`obj`の参照カウントは変化しない。

@param[in] obj プロキシオブジェクト
@retval obj オブジェクトへの接続が有効である。
@retval nullptr オブジェクトへの接続が無効または不明である。
@sa http://blogs.msdn.com/b/reuvenlax/archive/2005/11/08/490565.aspx
@sa https://web.archive.org/web/20151016061936/http://blogs.msdn.com:80/b/reuvenlax/archive/2005/11/08/490565.aspx
*/
_Ret_maybenull_ IUnknown* PingRemoteObject(_In_opt_ IUnknown* obj)
{
	static const GUID IID_UNDEFINED = /*適当なIID*/;

	if (obj != nullptr)
	{
		IUnknownPtr unk;
		HRESULT hr = obj->QueryInterface(IID_UNDEFINED, reinterpret_cast<void**>(&unk));
		switch (hr)
		{
		case S_OK: __fallthrough;
		case E_NOINTERFACE: __fallthrough;
		case RPC_E_SERVERCALL_RETRYLATER:
			return obj;
		default:
			return nullptr;
		}
	}
	else
	{
		return nullptr;
	}
}
```

# 前哨戦

さて、COMとATLが多用されたプログラムのVisual Studioのバージョンアップを行っていたときに前哨戦となる問題は起こりました。というか実はすでにそれについては記事を書いてましたってのを今思い出しました。

https://qiita.com/yumetodo/items/f53673959d1f6df0f4dc

で触れた`DECLARE_POLY_AGGREGATABLE`を指定した場合、対象クラスに`DECLARE_PROTECT_FINAL_CONSTRUCT`を指定しても効果は発揮されないために、`::CoCreateInstance`呼び出し時に呼ばれる`ATL::CComPolyObject::FinalConstruct`で自身のオブジェクトの参照カウントを増やすと次のようにDebug Assertion Failedが出るという問題を踏んでいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/1695e102-28f0-6abb-f836-c8588d8a48a6.png)

集約を使うのをやめて`DECLARE_NOT_AGGREGATABLE`にすることで解決を見ました

# 事象発生

そうして一息ついていた私達は、プログラムを長期間起動させていたところ、新たなトラブルに遭遇しました。

先の事象を踏んだCOMクラスは、各ユーザーセッションごとに生成する必要がありました。そしてCOMオブジェクトが生成されるときょうびあまり見なくなったツールバーを表示するという仕様になっています。またタスクトレイにアイコンを追加します。

長時間起動によって、このツールバーとタスクトレイアイコンが増殖していたのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/c03b0747-bea7-b5a4-c613-7c403a4b35b7.png)

(あくまでイメージ図であって、別にIMEを作ってるわけじゃないです)

# 再入

社内の有識者の解析の結果、どうも下のブログに解説があるような**再入**が悪いとわかりました。

[STAのメソッド呼び出しを見てみる - イグトランスの頭の中](https://dev.activebasic.com/egtra/2014/12/13/706/)

下に示しているのは先程

>ユーザーセッションはユーザーのログオンやログアウト、RDPの切断など様々な要因で変化します。そのため、WTS_SESSION_xxx系イベントや関係しそうな様々なイベントを契機として、次のMSDNをまねて適当なIIDでQueryInterfaceして反応を探るということをしています。

といった処理の具体的なものです。わかりやすいようにいらないところを省いて、高度にリファクタされている箇所を書き下して原始的にしてあります。

```cpp
class DECLSPEC_NOVTABLE UIAgentManager
// 略
{
	typedef boost::container::flat_map<DWORD, TypeLib::IUIAgentPtr> UIMap;
 	UIMap m_uiElement;
    void UpdateAgent(DWORD sessionId);
    void EraseOldElement();
// 略
};
```

```cpp
void UIAgentManager::UpdateAgent(DWORD sessionId)
{
    TypeLib::IUIAgentPtr& agent = m_uiElement[sessionId];

    if (Com::PingRemoteObject(agent) != nullptr)
    {
        // do something
    }
    else
    {
        agent = Helper::CreateSessionObject<TypeLib::UIAgent>(sessionId);
        agent->Initialize(Something());
        // do something
    }
}
void UIAgentManager::EraseOldElement()
{
	for (UIMap::iterator it = m_uiElement.begin(); it != m_uiElement.end(); )
	{
		if (it->second == nullptr)
		{
			it = m_uiElement.erase(it);
		}
		else
		{
			++it;
		}
	}
}
```

ここで`EraseOldElement`は非常に様々な契機で呼び出されます。また、UIAgentManagerはSTAで動作しています。

`m_uiElement[sessionId]`で`TypeLib::IUIAgentPtr`の参照を取得しています。そして、`Helper::CreateSessionObject<TypeLib::UIAgent>(sessionId)`の結果をそこに書きこんでいます。

このメソッドはSTAであり、`Helper::CreateSessionObject<TypeLib::UIAgent>(sessionId);`や`agent->Initialize(Something());`は別のアパートメントのため、呼び出しの間メッセージループを回して待機します。

一方で同時に何らかのイベントを契機として`EraseOldElement`が割り込むようにして呼び出されます。


1. `UpdateAgent`でsession Aの`TypeLib::IUIAgentPtr`の**参照**を取得する
1. そのポインタはnullptrであったために、`UIAgent`の生成をsession Aで行う。その間`UpdateAgent`側ではメッセージループを回す
1. 何らかのイベントを受信する関数の呼び出しが割り込んでくる
1. `EraseOldElement`が呼び出され、session Aの`TypeLib::IUIAgentPtr`を確認するとnullptrであったために、`m_uiElement`から削除をおこなう。
1. `UIAgent`の生成が終わり`UpdateAgent`に制御が戻ってくる。そして1で取得した参照に書き込もうとするがこれは`m_uiElement`の管理から外れた無効な参照である。ただし`m_uiElement`の予約しているメモリ空間であろうことからメモリアクセス違反とはならないと予想される。`m_uiElement`の管理から外れているということは、デストラクタを呼ばれる契機はないので、リークする。
1. そのうちsession Aで`UIAgent`の生成が再び行われ`m_uiElement`に登録される

すなわち再入の恐ろしさとは、**見た目には気が付きにくいところで勝手にメッセージループを回されてしまうがために勝手に別の処理が割り込んでくる**、というところにあります。こんなの初見で気がつけてたまるかシリーズです。

# 再入との戦い

## IMessageFilter → ボツ
再入と戦うに当たってまずは再入を防げないか考えます。

調べてみると`IMessageFilter`を用いて、防ぐのが一般的なようです。すなわちクライアントに対して「今忙しいから待って！」ということができるようです。

>[STAのメソッド呼び出しを見てみる - イグトランスの頭の中](https://dev.activebasic.com/egtra/2014/12/13/706/)
>
>なお、STAではメソッド・ウィンドウメッセージの受け付けをIMessageFilterとCoRegisterMessageFilterである程度制御できます。

ただし「ある程度」と書かれているのが不穏なところです。

他にも下のブログに紹介があるように、どうにもあまり信頼できる手法には見えません。

>[STA の難しさ](http://blogs.wankuma.com/melt/archive/0001/01/01/112869.aspx)
>
>ただ IMessageFilter にも落とし穴があって、「今忙しいからちょっと待って！」と言われたクライアントはどうするかというと、MFC であれば上記のリンク先にあるビジー状態のダイアログを出します。
>
>しかし IMessageFilter に対して特に何もしていない場合は、サーバが「今忙しいからちょっと待って！」と言っているにもかかわらず RPC_E_CALL_REJECTED というエラーを返します。
>
>これは IMessageFilter::RetryRejectedCall のデフォルトの処理がエラーを返すようになっているのが原因なので、クライアント側もちゃんと IMessageFilter を導入し、::CoRegisterMessageFilter で登録してやる必要があります。

## MTAにする → 見送り

そもそもMTAにすれば再入は発生しません。すなわち普通のマルチスレッドプログラミング同様、mutexでlockを取ればいいわけです。

ただこれはいろいろ考慮することが増えるので、時間もないので見送りとしました。

## そうだ、再入されても大丈夫なようにしよう！

今回の問題は無効となった領域への参照に書き込みをしようとするから`boost::container::flat_map`の管理から外れてしまうのが問題なのでした。

というわけで、そもそも参照を取得しなければ再入されても大丈夫じゃないの、ということができます。

```cpp
TypeLib::IUIAgentPtr UIAgentManager::CreateAgent(DWORD sessionId)
{
	TypeLib::IUIAgentPtr agent = Helper::CreateSessionObject<TypeLib::UIAgent>(m_wts, sessionId);
	agent->Initialize(Something());

	return agent;
}
void UIAgentManager::UpdateAgent(DWORD sessionId)
{
	// この関数の途中でコンテキストスイッチしたときに参照先が消えることがあるので参照を取得してはならない
	// TypeLib::IUIAgentPtr& agent = m_uiElement[sessionId];

	if (Com::PingRemoteObject(m_uiElement[sessionId]) != nullptr)
	{
		//do something
	}
	else
	{
		m_uiElement[sessionId] = CreateAgent(sessionId);
	}
}
```

## `DECLARE_NOT_AGGREGATABLE`ではなく`DECLARE_CLASSFACTORY_SINGLETON`を用いる

そもそも`UIAgent`はセッションごとに1つあればいいのでした。ということは`DECLARE_CLASSFACTORY_SINGLETON`を使えばいいのではないかという指摘が、社内の有識者よりなされました。

[ATL object map / singleton creation](https://forums.codeguru.com/showthread.php?278078-ATL-object-map-singleton-creation)

でもちょっとまってください。もともと前哨戦の結果`DECLARE_NOT_AGGREGATABLE`を使うようにしたはずです。それを変えても大丈夫でしょうか？

この時は `DECLARE_NOT_AGGREGATABLE`が無かったことで `ATL::CComPolyObject` だったため、`InternalFinalConstructRelease` が呼び出されずに参照カウンタの警告が発生していたのでした。

`atlcom.h`

`DECLARE_CLASSFACTORY_SINGLETON`
```cpp
#define DECLARE_CLASSFACTORY_SINGLETON(obj) DECLARE_CLASSFACTORY_EX(ATL::CComClassFactorySingleton<obj>)
```

`DECLARE_CLASSFACTORY_EX`
```cpp
#define DECLARE_CLASSFACTORY_EX(cf) typedef ATL::CComCreator< ATL::CComObjectCached< cf > > _ClassFactoryCreatorClass;
```

`ATL::CComCreator`
```cpp
template <class T1>
class CComCreator
{
public:
	static HRESULT WINAPI CreateInstance(
		_In_opt_ void* pv, 
		_In_ REFIID riid, 
		_COM_Outptr_ LPVOID* ppv)
	{
		ATLASSERT(ppv != NULL);
		if (ppv == NULL)
			return E_POINTER;
		*ppv = NULL;

		HRESULT hRes = E_OUTOFMEMORY;
		T1* p = NULL;

ATLPREFAST_SUPPRESS(6014 28197)
		/* prefast noise VSW 489981 */
		ATLTRY(p = _ATL_NEW T1(pv))
ATLPREFAST_UNSUPPRESS()

		if (p != NULL)
		{
			p->SetVoid(pv);
			p->InternalFinalConstructAddRef();
			hRes = p->_AtlInitialConstruct();
			if (SUCCEEDED(hRes))
				hRes = p->FinalConstruct();
			if (SUCCEEDED(hRes))
				hRes = p->_AtlFinalConstruct();
			p->InternalFinalConstructRelease();
			if (hRes == S_OK)
			{
				hRes = p->QueryInterface(riid, ppv);
				_Analysis_assume_(hRes == S_OK || FAILED(hRes));
			}
			if (hRes != S_OK)
				delete p;
		}
		return hRes;
	}
};
```

`DECLARE_CLASSFACTORY_SINGLETON` を指定しても `InternalFinalConstructRelease()` が呼び出されることがわかります。
よって `DECLARE_NOT_AGGREGATABLE` から `DECLARE_CLASSFACTORY_SINGLETON` への変更でもデグレはしていないと考えることができます。

# (一敗) P0145R3実装前は式の評価順序が不定だった

https://cpprefjp.github.io/lang/cpp17/expression_evaluation_order.html

`m_uiElement[sessionId] = CreateAgent(sessionId);`という式は`m_uiElement[sessionId]`の`operator[]`呼び出しと`CreateAgent`呼び出し、そして`operator=`の呼び出しの3要素で構成されていますが、これはどういう順番になるかが規定されたのはC++17のことでした。残念ながら今回はまだ実装されていなかったのでこうなりました。

```diff_cpp
-		m_uiElement[sessionId] = CreateAgent(sessionId);
+		m_uiElement.insert_or_assign(sessionId, CreateAgent(sessionId));
```

# (二敗)`boost::container::flat_map`ではイテレータループ中に再入が起きたときに問題となる

次のサンプルコードを見てください。擬似的に再入を体験するために途中でループをぶった切ってます。

```cpp
#include <unordered_map>
#include <boost/container/flat_map.hpp>
#include <iostream>
#include <string>
template<typename Map>
void on_map()
{
    Map map = {
        { 2, "arikitari" },
        { 7, "na" },
        { 51, "sekai" },
        { 21, "arikitari" },
        { 73, "na" },
        { 63, "sekai" },
        { 13, "arikitari" },
        { 27, "na" },
        { 12, "sekai" },
    };
    auto it = map.begin();
    for (int i = 0; i < 3; ++i, ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    std::cout<< map[55] << std::endl;
    for (; it != map.end(); ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
}
int main()
{
    on_map<std::unordered_map<int, std::string>>();
    std::cout << "-----------" << std::endl;
    on_map<boost::container::flat_map<int, std::string>>();
}
```

https://wandbox.org/permlink/9XzjCJv2LOGrUQWg

```
27,na
13,arikitari
63,sekai

73,na
21,arikitari
12,sekai
51,sekai
7,na
2,arikitari
-----------
2,arikitari
7,na
12,sekai

13,
21,
27,
51,
63,
73,
```

`boost::container::flat_map`ではイテレータを回している最中にinsertが走るとイテレータが無効となってしまうようです。というわけで`m_uiElement`には`std::unordered_map`を使うべきでした。

# (三敗)`std::unordered_map`もrehashされるとバグる。

>というわけで`m_uiElement`には`std::unordered_map`を使うべきでした。

と書いたな、アレは嘘だっ！次のサンプルコードを見てください。

```cpp
#include <unordered_map>
#include <iostream>
#include <string>
template<typename K, typename V>
void print_bucket(const std::unordered_map<K, V>& map)
{
    constexpr const char* s = "[,";
    std::cout << "bucket::" << map.bucket_count() << ':';
    for(std::size_t i = 0; i < map.bucket_count(); ++ i) {
        std::cout << s[i != 0] << map.bucket_size(i);
    }
    std::cout << ']' << std::endl;
}
void on_map()
{
    using Map = std::unordered_map<int, std::string>;
    Map map = {
        { 2, "arikitari" },
        { 7, "na" },
        { 51, "sekai" },
        { 21, "arikitari" },
        { 73, "na" },
        { 63, "sekai" },
        { 13, "arikitari" },
        { 27, "na" },
        { 12, "sekai" },
    };
    auto it = map.begin();
    for(auto&& [k, v] : map) std::cout << k << ',' << v << std::endl;
    std::cout << "-----------" << std::endl;
    it = map.begin();
    for (int i = 0; i < 3; ++i, ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    const auto bc = map.bucket_count();
    print_bucket(map);
    std::cout<< map[55] << std::endl;
    for(int i = 100; map.bucket_count() == bc; ++i) map[i];
    print_bucket(map);
    for (; it != map.end(); ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
}
int main()
{
    on_map();
}
```

https://wandbox.org/permlink/lW8W6CC1rLLb388w

```
27,na
13,arikitari
63,sekai
73,na
21,arikitari
12,sekai
51,sekai
7,na
2,arikitari
-----------
27,na
13,arikitari
63,sekai
bucket::13:[1,1,1,0,0,0,0,1,2,0,0,1,2]

bucket::29:[0,0,1,0,0,1,0,1,0,0,0,0,1,2,1,2,1,0,0,0,0,1,1,0,0,0,1,1,0]
73,na
102,
27,na
55,
13,arikitari
100,
101,
```

`unordered_map`は要素が増えていってbucketの大きさを超えるとrehashします。このときイテレータは死にます。

というわけで`m_uiElement`には`std::map`を使うべきでした。

# (四敗)そもそもイテレータループ中に要素削除が起こると`std::map`といえど死ぬ

```cpp
#include <map>
#include <iostream>
#include <string>
int main()
{
    std::map<int, std::string> map = {
        { 2, "arikitari" },
        { 7, "na" },
        { 51, "sekai" },
        { 21, "arikitari" },
        { 73, "na" },
        { 63, "sekai" },
        { 13, "arikitari" },
        { 27, "na" },
        { 12, "sekai" },
    };
    auto it = map.begin();
    for (int i = 0; i < 3; ++i, ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    map[51] = {};
    std::cout<< map[51] << std::endl;
    for (; it != map.end(); ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }

    std::cout << "--------------" << std::endl;
    it = map.begin();
    for (int i = 0; i < 3; ++i, ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    std::cout<< map[55] << std::endl;
    for (; it != map.end(); ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    std::cout << "--------------" << std::endl;
    it = map.begin();
    for (int i = 0; i < 3; ++i, ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    map.erase(27);
    for (; it != map.end(); ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    std::cout << "--------------" << std::endl;
    it = map.begin();
    for (int i = 0; i < 3; ++i, ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
    map.erase(13);
    std::cout<< map[13] << std::endl;
    for (; it != map.end(); ++it) {
        std::cout << it->first << ',' << it->second << std::endl;
    }
}
```

https://wandbox.org/permlink/epDkP4QRlgQM2EKu

```
2,arikitari
7,na
12,sekai

13,arikitari
21,arikitari
27,na
51,
63,sekai
73,na
--------------
2,arikitari
7,na
12,sekai

13,arikitari
21,arikitari
27,na
51,
55,
63,sekai
73,na
--------------
2,arikitari
7,na
12,sekai
13,arikitari
21,arikitari
51,
55,
63,sekai
73,na
--------------
2,arikitari
7,na
12,sekai

/opt/wandbox/gcc-13.2.0/include/c++/13.2.0/debug/safe_iterator.h:472:
In function:
    bool gnu_debug::operator==(const 
    _Safe_iterator<std::_Rb_tree_iterator<std::pair<const int, std::
    cxx11::basic_string<char> > >, std::debug::map<int, std::
    cxx11::basic_string<char> >, std::forward_iterator_tag>::_Self&, const 
    _Safe_iterator<std::_Rb_tree_iterator<std::pair<const int, std::
    cxx11::basic_string<char> > >, std::debug::map<int, std::
    cxx11::basic_string<char> >, std::forward_iterator_tag>::_Self&)

Error: attempt to compare a singular iterator to a past-the-end iterator.

Objects involved in the operation:
    iterator "lhs" @ 0x7fff8c0c3dd0 {
      type = std::_Rb_tree_iterator<std::pair<int const, std::cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > (mutable iterator);
      state = singular;
      references sequence with type 'std::debug::map<int, std::cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::less<int>, std::allocator<std::pair<int const, std::cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >' @ 0x7fff8c0c3e00
    }
    iterator "rhs" @ 0x7fff8c0c4110 {
      type = std::_Rb_tree_iterator<std::pair<int const, std::cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > (mutable iterator);
      state = past-the-end;
      references sequence with type 'std::debug::map<int, std::cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::less<int>, std::allocator<std::pair<int const, std::cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >' @ 0x7fff8c0c3e00
    }

```

それはそうみたいな話ですが、挿入にはめっぽう強い`std::map`も削除の前には無力です。

というわけでそもそもイテレータループを回したいときはイテレータループ中での再入を避けるために、予め`map`そのものをコピーして、それのループを回すべきでした。

# 終わりに

再入というのは実に厄介な事象で、特に今回のように真っ向から再入を許容しながら抗う場合には極めて苦しい戦いを強いられます。

多分MTAにしたほうが遠回りでも安心安全だと思います、まる。

あとやはりCOMは人類には早すぎたのではないかと思います。とても辛いです。上に書いたことも多分に知ったかぶりが紛れています。

# 補足説明

現在進行系でバグと戦っていたときにこの記事を書いたので、だいぶ説明が投げやりでした。なのでもうすこし補足説明をします。

## COMを今から学ぶ難しさ

現代、0からCOMを学ぶのは難しくなっています。現役でCOMを書いていた人たちのうち、その殆どはCOMからすでに足を洗っています。DirectXを触るようなゲーム業界の一部ならCOMを学ぶ環境があるのかもしれませんが、そうではない場合、多くは当時発売されたCOMとATLに関する書籍を図書館で探して、あるいは他館回送してもらって、熟読することとなろうかと思います。あるいは偶にAmazonなどで出てくる中古の本を手に入れることでしょうか(自分はこっちでした)。

ネットでの解説は頑張って探せばあります。必読となるのは以下ではないでしょうか。

- [ChalkTalk CLR – COMのすべて – kekyoの丼](https://www.kekyo.net/2015/12/21/5510)
    - [COMのアパートメント (1) スレッド同期の隠蔽 – kekyoの丼](https://www.kekyo.net/2011/06/05/14)
    - [COMのアパートメント (2) コンポーネントファサード – kekyoの丼](https://www.kekyo.net/2011/06/08/30)
    - [COMのアパートメント (2) コンポーネントファサード – kekyoの丼](https://www.kekyo.net/2011/06/08/30)
    - [COMのアパートメント (3) スレッドの属性とコンポーネント – kekyoの丼](https://www.kekyo.net/2011/06/17/33)
    - [COMのアパートメント (4) スレッド親和性 – kekyoの丼](https://www.kekyo.net/2011/07/07/47)
    - [COMのアパートメント (5) CoInitializeExは初期化ではない – kekyoの丼](https://www.kekyo.net/2012/06/16/80)
    - [COMのアパートメント (6) アパートメントの種類はどのように決まるのか – kekyoの丼](https://www.kekyo.net/2013/07/22/382)
- [COMのカレンダー | Advent Calendar 2014 - Qiita](https://qiita.com/advent-calendar/2014/com)
    - @egtra 氏による一人Advent Calender。本記事に関連が深い記事としては以下。
    - [参照カウントしないIUnknown - イグトランスの頭の中](https://dev.activebasic.com/egtra/2014/12/03/692/)
    - [COMにおけるアパートメントの概要 - イグトランスの頭の中](https://dev.activebasic.com/egtra/2014/12/10/703/)
    - [ATL::CComGITPtrを使ってみる - イグトランスの頭の中](https://dev.activebasic.com/egtra/2014/12/12/705/)
    - [STAのメソッド呼び出しを見てみる - イグトランスの頭の中](https://dev.activebasic.com/egtra/2014/12/13/706/)
- [COM プログラミング入門 - Web/DB プログラミング徹底解説](https://www.keicode.com/winprimer/index6.php)
    - [クラスファクトリの実装 ～ 作ってわかる COM の基礎 - Windows プログラミング入門 - Web/DB プログラミング徹底解説](https://www.keicode.com/winprimer/com-basics1-4-classfactory.php)

COMをC++から扱うときに欠かせないのがATLですが、ATLについて学習するのはそれこそ困難を伴うと感じています。今回COMを学ぶに当たっては、なるべくシンプルなCOMオブジェクトをATLで作って、それを`::CoCreateInstance`で呼び出してみたり、ひたすらATLの内部実装を読んだり、すでにあるコードをデバッガーでステップ実行して追いかけたりしていましたが、もうすこし体系的な学習手段がほしいところです。今度中古本を探してみようと思います。

## 再入についての補足

STAにおいて、別のアパートメント(例えば別のスレッドとか別のプロセスとか)のCOMオブジェクトのメソッドを呼び出すことを考えます。

このとき、呼び出し中は処理をブロッキングしてしまうとまずいわけです。例えば自身のアパートメントがGUIを表示させていたとして、ボタンのクリックとかそういうのに応答できないとフリーズしていているように見えてしまいますよね。

そこでSTAにおいては、別のアパートメントのメソッドを呼び出している間、メッセージループを回します。`GetMessage`して`TranslateMessage`して`DispatchMessage`しているあれを思い浮かべればいいと思います。

```cpp
    MSG msg;
    for (;;)
    {
      int ret = GetMessage(&msg, nullptr, 0, 0);
      if (ret == 0 || ret == -1)
        break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
```

>[COMのアパートメント (6) アパートメントの種類はどのように決まるのか – kekyoの丼](https://www.kekyo.net/2013/07/22/382)
>
>![](https://i0.wp.com/www.kekyo.net/wp-content/uploads/2013/07/apartments1.png?w=1024&ssl=1)



すると何が起きるかというと、ある種のコンテキストスイッチが起こるわけです。まるでコルーチンみたいですよね。つまり、呼び出し元の処理は一旦中断されて、他の処理を受け付け、呼び出しが終わったことが通知されると、他の処理からメッセージループに遷移したときに再び処理を再開するわけです。

すなわち、シングルスレッドだけれども、シーケンシャルに処理されないことがあるわけです。

ここで他の処理と自身の処理で同一の状態を参照している、具体的には同じ変数に対して操作をしていると、まるでマルチスレッドプログラミングにおける競合状態に対する考慮と同様にことが必要になるわけです。それがまさにこの記事で戦った相手です。

もちろんシングルスレッドなので同じ変数に対して操作していても、直ちにそれが問題にはなりません。しかし、相互に及ぼす影響は整理しなければなりません。

ここで厄介なのはマルチスレッドプログラミングにおいて通用する「とりあえずmutexでロックを確実に取る」ができないということです。なぜならば自身の処理をブロッキングした瞬間、シングルスレッドであるがゆえに、それを抜け出す契機はないからです。

## 再入に対する対策の選択肢

今回はやりませんでしたが、再入に対する対策の第一選択肢は、STAを捨てることであるべきです。すなわちMTAにするということです。こうすることで、単なるマルチスレッドプログラミングとなり、「とりあえずmutexでロックを確実に取る」を行うだけでよくなります。

第二選択肢は処理をリトライすることだと思います。つまり、`SetWaitableTimer`などを用いて、自身の処理を再び呼び出すタイマーを0秒後に設定します。タイマーからのシグナルを受け取るのは別のスレッドでしょうが、そこから自身の処理を呼び出すと、それはSTAのメソッド呼び出しですから、メッセージループへの通知となります。

## なぜリトライを選ばなかったか

じつは今回の問題に対して社内の有識者から示された改修方針の第一案はこれでした。すなわち、boolフラグをmutexに見立てて`m_uiElement`に触る前にtry_lockしだめなら即時発火のタイマーでリトライをかけることでした。タイマーをかませることでその間に再びCOMの再入を誘発させてロックが外れることを期待します。ただタイマーはそれ自体バグを生みやすいものであることから一旦却下しました。

かわりに、`EraseOldElement`があちこちから呼ばれるのはそもそも問題を悪化させていたので、一定時間おきのタイマー契機のみで`EraseOldElement + UpdateAgent`を1ループで回すように書き換えました(下記`ReconnectUIProcess`)。そこ以外では要素削除をしないで、当該saessinIdの要素にnullptr代入だけにしていました。削除契機を1つに絞り込めば大丈夫だろうと考えたわけです。

```cpp
void UIAgentManager::ReconnectUIProcess()
{
	for (UIMap::iterator it = m_uiElement.begin(); it != m_uiElement.end(); )
	{
        const auto sessionId = it->first;
        try {
			if (it->second == nullptr)
			{
				it = m_uiElement.erase(it);
				continue;
			}
			if (Com::PingRemoteObject(it->second) == nullptr)
			{
				it->second = nullptr; // CreateAgentが失敗してもいいようにまず空にする
				it->second = CreateAgent(sessionId);
			}
		}
		catch (const std::exception& er) {
			// logging...
		}
		++it;
	}
}
```

`it->second`はユーザーセッションで動くCOMオブジェクトのポインタ(`_com_ptr_t`)ですが、`nullptr`ということはそのオブジェクトは開放されているわけです(他で参照カウントを増やしてない限り)。ならば削除契機を絞り込んでも、瞬間的に多少`m_uiElement`が肥大化するくらいで済むというわけです。

三敗目では、`m_uiElement`に対するイテレータループがそれまで`EraseOldElement`を呼び出していたのと同じだけたくさんあったことに問題がありました。すなわち削除契機を1契機に絞り込んでもなおイテレータループが壊れうるわけです。なのでstd::mapなら削除でも壊れないと思い込んだのです。しかしそんなはずもなく4敗目に至りました。

こうして再び第一案を検討し始めますが、すぐにリトライに至るケースが多すぎることに頭を抱えます。せっかく削除契機を1契機に絞り込んだのにもったいないです。

そこで考えた第二案はシングルスレッドのRW lockもどきを作って同様に対処することでした。削除契機は大幅削減したので、大抵のケースではロックを取得できるはずです。
しかしながら実装が複雑になる割に第一案の問題を解決しきれません。よって却下しました。

とはいえどうしたものかと、なおも第一、第二案でぐるぐると悩むこと2日、そもそも`m_uiElement`はユーザーセッションに対応する辞書であることを思い出します。

じつはWindows Serverは開発している製品のサポート対象外でした(少し前までWindows Server2012/R2をサポートしていた)。Server版ではないWindowsではユーザーセッションの数は定数であり十分に少ない(というか1つ程度)です。要素型も_com_ptr_tなのでコピーコストは別に避けなくていいということです

かくして

>予めmapそのものをコピーして、それのループを回すべきでした。

となったのでした。

ただ今にして思うと更に別の選択肢があるのではないかと思えてきました。

第二案のRW lockもどきの亜種で、それぞれのイテレータループに入るときにフラグを立てておき、`ReconnectUIProcess`の`if (it->second == nullptr)`の分岐内でそのフラグをみてフラグが立ってたら削除をスキップするというものです。`ReconnectUIProcess`はタイマー契機で何度も呼ばれるので、いつかはスキップされずに削除が行われるでしょうから、`m_uiElement`の肥大化も問題にはならないわけです。

もしWindows Serverをサポート対象OSとする場合、一台のPCに数十ユーザーが頻繁にRDP経由でログインしてくる、なんてケースも考えられますので、`m_uiElement`はコピーするにはすこし気になるくらい大きくなる可能性もあるかもしれないので、この案のほうが良かったかもしれませんが、まあどうせServerOSはサポートしないのでそこまでしなくていいでしょう。


# 続編

[2024年の今、いかにしてVS2005を捨ててVS2015にする戦いは終わったか、そしてなぜCOMとの苦しい戦いが繰り広げられたか ～再入の悪魔～ - OPTiM TECH BLOG](https://tech-blog.optim.co.jp/entry/2024/03/28/100000)

https://tech-blog.optim.co.jp/entry/2024/03/28/100000

# さらなる続編

[それはCOM STAと並列処理の三体問題との戦いだった ～Optimal Biz Teleworkの機能をOptimal Bizに部分取り込みする～ - OPTiM TECH BLOG](https://tech-blog.optim.co.jp/entry/2024/12/19/100000)

https://tech-blog.optim.co.jp/entry/2024/12/19/100000