std :: functionを回避するためのC ++ 20の概念の使用

Aug 22 2020

以前は、関数パラメーターとしてコールバックが必要だったとき、通常はを使用することにしましたstd::function。キャプチャを絶対に使用しないというまれなケースでは、typedef代わりに関数宣言にを使用しました。

したがって、通常、コールバックパラメータを使用した宣言は次のようになります。

struct Socket
{
  void on_receive(std::function<void(uint8_t*, unsigned long)> cb);
}

ただし、私が知る限り、std::function実際には、キャプチャを使用してラムダをstd::functionテンプレートに解決し、キャプチャを移動/コピーする必要があるため、実行時に少し作業を行っています(?)。

私が考えた新しいC ++ 20機能について読むstd::functionと、実行可能なファンクターの制約付きパラメーターの使用を回避し、使用するための概念を利用できる可能性があります。

そして、ここで私の問題が発生します。将来、コールバックファンクターオブジェクトを操作したいので、それらを保存する必要があります。コールバックの明確なタイプがないため、最初に考えたのは、ファンクターをコピーして(最終的にはある時点で移動して)ヒープにコピーし、を使用しstd::vector<void*>てそれらを残した場所をメモすることでした。

template<typename Functor>
concept ReceiveCallback = std::is_invocable_v<Functor, uint8_t*, unsigned long>
                       && std::is_same_v<typename std::invoke_result<Functor, uint8_t*, unsigned long>::type, void>
                       && std::is_copy_constructible_v<Functor>;
struct Socket
{
  std::vector<void*> callbacks;

  template<ReceiveCallback TCallback>
  void on_receive(TCallback const& callback)
  {
    callbacks.push_back(new TCallback(callback));
  }
}

int main(int argc, char** argv)
{
  Socket* sock;
  // [...] inialize socket somehow

  sock->on_receive([](uint8_t* data, unsigned long length)
                   {
                     // NOP for now
                   });

  // [...]
}

これは十分に機能しますが、ファンクターを呼び出すことになっているメソッドを実装するときに、不明/欠落タイプの問題を延期したことに気付きました。私の理解では、void*関数ポインタまたは同様のハックにaをキャストすると、UBが生成されます-コンパイラは、完全に未知のクラスのoperator()を実際に呼び出そうとしていることをどのように知るのでしょうか?

(コピーされた)ファンクターをそのoperator()定義への関数ポインターと一緒に保存することを考えましたthisが、関数内にファンクターを挿入する方法がわかりません。それがなければ、キャプチャーが機能するかどうかは疑問です。

私が持っていた別のアプローチは、必要なoperator()関数を宣言する純粋仮想インターフェースを宣言することでした。残念ながら、私のコンパイラーは私のファンクターをインターフェースにキャストすることを禁じており、ラムダをそれから派生させる合法的な方法もないと思います。

それで、これを解決する方法はありますか、それともテンプレートの要件/概念機能を誤用している可能性がありますか?

回答

13 NicolBolas Aug 22 2020 at 00:35

あなたの最初のバージョンを使用std::function正確にので、それはタイプが消去されます。型消去が必要な場合(そして、コードがその型が何であるかを明示的に知らなくてもユーザーが任意の型を使用できるようにしたいので、明らかにそうします)、何らかの形式の型消去が必要です。そして、型消去は無料ではありません。

制約はテンプレート用です。テンプレート関数は必要ありません。型が消去された呼び出し可能オブジェクトを処理する単一の関数が必要です。

また、プロバイダーのコールスタックよりも長持ちする必要があるコールバックの場合、std::functionのオーバーヘッドはほとんど必要なものです。つまり、「オーバーヘッド」は無意味ではありません。これにより、任意の不明なタイプのオブジェクトをコールバックプロセッサに格納できます。