はじめに
現在 TS に提案されている Networking Library は Boost.Asio をベースにしていますが, Boost.Asio そのままというわけではなく様々な変更が含まれています. 本記事ではそれらの変更点によって何が変わったのかを説明しようと思います. 最初は Associated allocator について説明します.
なお, 本記事の内容は何かの議論等の議事録を見て書いたということはなく, ただの筆者の考えで誤りがある可能性が多分にありますのでその点注意してください.
本記事で参照する Networking Library の文書は, Networking Library (Revision 7) です.
Boost.Asio でのメモリアロケーション
Boost.Asio では非同期関数 (socket::async_write_some や timer::async_wait など) を呼び出した際に, 引数で渡したハンドラ等を保持するためにメモリ確保を行います. そして, 非同期処理が完了してハンドラが呼び出される直前で確保したメモリを解放します.
このメモリの確保と解放はそれぞれ asio_handler_allocate と asio_handler_deallocate という非メンバ関数によって行われます. どちらの関数も引数にハンドラを指すポインタを取るので, これらのメモリ確保/解放は ADL によってカスタマイズ可能となっています*1.
Networking TS でのメモリアロケーション
Networking TS でも Boost.Asio のときと同様にメモリの確保/解放が行われます. しかし, 前述した asio_handler_allocate/deallocate は存在しておらず, 代わりに Associated allocator を使用してメモリの確保/解放を行います.
Associated Allocator とはハンドラに関連付けされたアロケータのことで, 具体的には associated_allocator というクラステンプレートの static メンバ関数 get で取得できるアロケータです.
my_handler h;
auto alloc = associated_allocator<my_handler>::get(h);
ユーザは以下の方法でアロケーションをカスタマイズできます.
- associated_allocator を自身が作成したハンドラの型で特殊化する, または,
- ハンドラの型にメンバ関数 get_allocator と allocator_type というネスト型を定義する.
カスタマイズしない場合は std::allocator
Associated allocator に変更された理由
Boost.Asio におけるハンドラの CopyConstructible の制約
Boost.Asio ではハンドラの型はコピーコンストラト可能である必要があると ドキュメント に記載されています. この記載はムーブがない時代に書かれたからではありません.
このコピーの理由を理解するために非同期関数の内部でどのように asio_handler_allocate/deallocate を使用しているかを見てみます. 以下がそのコードです*2.
handler h; typedef ... inner_handler; void* p = asio_handler_allocate(sizeof(inner_handler), std::addressof(h)); try { inner_handler* inner_h = new(p) inner_handler(h, ...); // コピーコンストラクト } catch (...) { asio_handler_deallocate(p, std::addressof(h)); // ここで h を必要としている throw; }
inner_handler 型は非同期関数の内部で使用される型で, メンバ変数としてその非同期関数に必要な情報 (Socket の書き込みバッファなど) とユーザが指定したハンドラを保持しています. そしてこのメンバ変数のハンドラはコピーコンストラクトによって構築されます.
コピーの代わりにムーブを使う場合を考えてみます. inner_handler のコンストラクタに h をムーブで渡すと h の状態は不定になります. そして, もし, inner_handler のコンストラクタで例外が投げられると, asio_handler_deallocate は状態が不定な h にアクセスすることになってしまいます. 例外安全の基本保証を満たすにはムーブではなくコピーで inner_handler を構築する必要があるのです.
上記のことはハンドラ呼び出し直前のメモリ解放処理にも当てはまります.
bool need_deallocate = true; try { handler h(inner_h->handler); // コピーコンストラクト need_deallocate = false; inner_h->~inner_handler(); asio_handler_deallocate(inner_h, &h); h(args); } catch (...) { if (need_deallocate) { void* p = inner_h; asio_handler_deallocate(p, &inner_h->handler); // inner_h->handlr が必要 } throw; }
Associated allocator の場合
Associated allocator を使った場合どうなるか見てみましょう.
handler h; auto alloc = get_associated_allocator(h); using inner_handler = ...; void* p = alloc.allocate(sizoef(inner_handler)); try { auto inner_h = new(p) inner_handler(std::move(h), ...); // ムーブコンストラクト } catch (...) { alloc.deallocate(p); // h は必要ない throw; }
Networking TS の場合, メモリ管理の責務をハンドラオブジェクトから別オブジェクト (Associated Allocator) に切り出しています. そのため, メモリを解放する際にはもうハンドラ h は必要ありません. このため, inner_handler の構築時に h をムーブしても問題ないことになります.
アロケータ内部の作りがハンドラに依存している場合は再び同じ問題にぶつかります. しかし, メモリ確保にハンドラ h のアロケータを使用して, ハンドラ呼び出し直前の解放処理では inner_h->handler のアロケータを使用するのでそのようなアロケータの実装は考えにくいです (確保と解放のアロケータそれぞれが異なるオブジェクトに依存することになる).
まとめ
Networking TS ではカスタムメモリアロケーションの仕組みにアロケータという層を挟むことで, メモリの確保/解放処理とハンドラの状態とを分離することに成功しました. これにより, 従来ハンドラの型はコピーコンストラクト可能であるという制約を, ムーブコンストラクト可能にまで緩和しました.
Boost.Asio ではハンドラに shared_from_this() で生成した shared_ptr を持たせるケースが多々あるので, shared_ptr のコピーを避けられるという意味でこの変更は実行速度の面でも意味のあるものになるのではないかと思います.
次は async_result の変更点について説明しようと思います.
*1:Custom Memory Allocation - 1.60.0
*2:実際の Boost.Asio のコードでは try-catch ではなく RAII を使用しています