この不適合性は、std::uses_allocator型特性から生じます。この特性は、std::stringとstd::pmr::polymorphic_allocatorの組み合わせについてfalseとなります。std::stringはそのallocator_typeをstd::allocator<char>としてハードコーディングしており、一方でstd::pmr::vectorはstd::pmr::polymorphic_allocator<char>を提供しています。これらは、暗黙の変換や継承関係がない異なる無関係なクラス型です。コンテナが要素を構築する際、std::uses_allocator_v<T, Alloc>をクエリして、アロケータをコンストラクタ引数として渡すかどうかを決定します。このチェックが失敗するため、ベクタはstd::stringをアロケータを認識しないものと見なし、デフォルトコンストラクタを呼び出します。このコンストラクタは、ベクタのメモリリソースに関係なく、内部でグローバルなnewとdeleteを使用します。
static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vectorはstd::stringにアロケータを渡しません
金融リスク計算エンジンの最適化中に、スタックメモリをバックに持つstd::pmr::monotonic_buffer_resourceを使用するようホットパスをリファクタリングしました。すべての一時的なシンボル名が単調バッファから取得されることを期待して、std::pmr::vectorstd::string temp_symbolsを宣言しましたが、パフォーマンスプロファイリングではstd::stringコンストラクタ内で予期しないmallocコールが表示され、メモリリソースが完全に回避されていることが示されました。
すべてのstd::stringを、そのコンストラクタにstd::pmr::polymorphic_allocatorを明示的に渡して手動で構築することを検討しましたが、これにはより高レベルのビジネスロジックにアロケーションの詳細を晒す必要があり、emplace_backのような便利な修飾子の使用を妨げました。別のアプローチとして、std::stringから継承し、ポリモルフィックアロケータを受け入れるカスタム文字列ラッパーを作成することも考えましたが、これはリスコフの置換原則に違反し、コンテナの再確保中にオブジェクトスライスのリスクを引き起こしました。最終的に、std::stringをstd::pmr::string(std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>のエイリアス)に置き換えました。これにより、ベクタは自動的にuses_allocatorプロトコルを通じてアロケータを伝播させ、ホットパスのすべてのヒープ割り当てを排除し、遅延をマイクロ秒から数百ナノ秒に削減しました。
内部で動的割り当てを行うカスタムクラスをstd::pmr::polymorphic_allocatorと互換性を持たせるにはどうすればよいでしょうか?単にコンストラクタにアロケータパラメータを受け入れることは不十分です。
クラスは、自身のアロケータ認識を明示的に宣伝する必要があります。これは、使用中のアロケータから変換可能な公共のallocator_type型エイリアスを公開するか、最初のパラメータがstd::allocator_arg_tで、2番目のパラメータがアロケータ型であるコンストラクタを提供し、std::uses_allocator<ClassName, Alloc>をstd::true_typeから派生させて特化させることによって行われます。この明示的な宣伝がない限り、std::pmr::vectorはクラスがアロケータを認識していないと見なし、デフォルト初期化を通じて構築し、内部のすべての割り当てがポリモルフィックメモリリソースをバイパスします。
なぜstd::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U>はstd::pmr::vectorとstd::stringの間の不適合性を解決しないのでしょうか?
再バインドはstd::pmr::polymorphic_allocator<U>を生成しますが、これはstd::allocator<U>とは依然として互換性がありません。なぜなら、両者は変換関係のない異なる具体型だからです。std::uses_allocatorメカニズムは、要素のallocator_typeがコンテナのアロケータ型と同じか、またはそこから変換可能である必要があり、単に異なる値型に再バインド可能であるだけでは不十分です。std::stringはstd::allocatorをハードコーディングしているため、コンテナのアロケータを再バインドしても、要素の期待されるアロケータ型は変わりません。
なぜstd::pmr::stringを使用することで解決されない特定のライフタイムリスクが存在するのでしょうか?また、その検出が標準アロケータよりも難しい理由は何でしょうか?
std::pmr::polymorphic_allocatorは型消去されており、基本のstd::pmr::memory_resourceへのポインタを保持しています。このため、コンパイラはコンパイル時にライフタイム制約を強制できません。スタックベースのmonotonic_buffer_resourceを参照しているstd::pmr::stringが、より長いライフタイムスコープに移動またはコピーされると、メモリリソースへのポインタがダングリング状態になります。標準のstd::allocatorは通常グローバルヒープ(常に有効)を使用するため、このような問題は発生しませんが、バッファが破棄された後に文字列にアクセスすると、使用後の解放が発生します。静的アナライザは、仮想のdo_allocate/do_deallocateインターフェイスが基礎となるリソースのライフタイムを型システムから隠すため、これを検出するのが困難です。