2

私は、再帰可能なラムダ自己スコープのクリーンできちんとした実装を構築しようとしています (これは基本的に Y コンビネーターですが、技術的には完全ではないと思います)。これは、他の多くのスレッドの中でも、このスレッド、このスレッド、およびこのスレッドに私連れて行った旅です。

問題の 1 つをできる限り簡潔に要約しました。テンプレート パラメーターとしてラムダを使用するテンプレート化されたファンクターをどのように渡すのですか?

#include <string>
#include <iostream>
#define uint unsigned int

template <class F>
class Functor {
public:
    F m_f;

    template <class... Args>
    decltype(auto) operator()(Args&&... args) {
        return m_f(*this, std::forward<Args>(args)...);
    }
};
template <class F> Functor(F)->Functor<F>;

class B {
private:
    uint m_val;
public:
    B(uint val) : m_val(val) {}
    uint evaluate(Functor<decltype([](auto & self, uint val)->uint {})> func) const {
        return func(m_val);
    }
};

int main() {
    B b = B(5u);
    Functor f = Functor{[](auto& self, uint val) -> uint {
        return ((2u * val) + 1u);
    }};

    std::cout << "f applied to b is " << b.evaluate(f) << "." << std::endl;
}

上記のコードは機能しません。Visual Studio はf(b.evaluate(f)呼び出しで) パラメーターの型と一致しないと主張しています。

私の仮定は、auto & selfこれを機能させるのに十分賢くないということです。どうすればこれを回避できますか? これらが本質的に定義できない場合、これらのものをどのように保存して渡すのですか? これが、私が見た Y-combinator の実装の多くが奇妙な二重ラップを持っている理由ですか?

どんな助けや説明も大歓迎です。

4

2 に答える 2

5

私が見る唯一の方法はevaluate()、テンプレート メソッドを作成することです。を確実に受け取りたい場合Functor(ただし、単純に callable を受け入れることができます: Yakk の回答を参照してください):

template <typename F>
uint evaluate(Functor<F> func) const {
    return func(m_val);
}

次の簡単なコードで確認できるように、すべてのラムダが異なる型であることを考慮してください

auto l1 = []{};
auto l2 = []{};

static_assert( not std::is_same_v<decltype(l1), decltype(l2)> );

evaluate()次の例でわかるように、(明らかに) 同じラムダ関数でメソッドを呼び出すと、呼び出しが一致しないため、特定のラムダ型を機能させることができません。

auto l1 = []{};
auto l2 = []{};

void foo (decltype(l1))
 { }

int main ()
 {
   foo(l2); // compilation error: no matching function for call to 'foo'
 }
于 2021-02-05T17:10:50.927 に答える
3

最も簡単な解決策は次のとおりです。

uint evaluate(std::function<uint(uint)> func) const {
    return func(m_val);
}

ステップアップは、を書くことfunction_viewです。

uint evaluate(function_view<uint(uint)> func) const {
    return func(m_val);
}

(ネット上には数十の実装があり、簡単に見つけることができます)。

最も簡単で最も実行効率の高い方法は次のとおりです。

template<class F>
uint evaluate(F&& func) const {
    return func(m_val);
}

私たちは何が何であるかを気にしないのでfunc、アヒルのように鳴きたいだけです。早めにチェックしたい方は…

template<class F> requires (std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >)
uint evaluate(F&& func) const {
    return func(m_val);
}

を使用する、または

template<class F,
  std::enable_if_t<(std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >), bool> = true
>
uint evaluate(F&& func) const {
    return func(m_val);
}

これは似ていますが、よりあいまいです。

fix-signature type-erased を書くこともできますがFunctor、それは悪い考えだと思います。次のようになります。

template<class R, class...Args>
using FixedSignatureFunctor = Functor< std::function<R( std::function<R(Args...)>, Args...) > >;

またはわずかに効率的

template<class R, class...Args>
using FixedSignatureFunctor = Functor< function_view<R( std::function<R(Args...)>, Args...) > >;

しかし、これはかなり正気ではありません。が何であるかを忘れたいと思うでしょうが、 !Fを置き換えることができるわけではありません。F

これを完全に「便利」にするには、スマート コピー/移動/割り当て操作を に追加する必要があります。各操作内の をFunctorコピーできる場合はFコピーできます。

template <class F>
class Functor {
public:
  // ...
  Functor(Functor&&)=default;
  Functor& operator=(Functor&&)=default;
  Functor(Functor const&)=default;
  Functor& operator=(Functor const&)=default;

  template<class O> requires (std::is_constructible_v<F, O&&>)
  Functor(Functor<O>&& o):m_f(std::move(o.m_f)){}
  template<class O> requires (std::is_constructible_v<F, O const&>)
  Functor(Functor<O> const& o):m_f(o.m_f){}
  template<class O> requires (std::is_assignable_v<F, O&&>)
  Functor& operator=(Functor<O>&& o){
    m_f = std::move(o.mf);
    return *this;
  }
  template<class O> requires (std::is_assignable_v<F, O const&>)
  Functor& operator=(Functor<O> const& o){
    m_f = o.mf;
    return *this;
  }
  // ...
};

( バージョン、required 句をstd::enable_if_t以前の SFINAE ハックに置き換えます)。

決定方法

ここで覚えておくべき重要なことは、C++ には複数の種類のポリモーフィズムがあり、間違った種類のポリモーフィズムを使用すると多くの時間を無駄にすることです。

コンパイル時ポリモーフィズムと実行時ポリモーフィズムの両方があります。コンパイル時ポリモーフィズムのみが必要な場合に実行時ポリモーフィズムを使用するのは無駄です。

次に、各カテゴリには、さらに多くのサブタイプがあります。

std::functionランタイム ポリモーフィック タイプの消去通常オブジェクトです。継承ベースの仮想関数は、もう 1 つのランタイム ポリモーフィック手法です。

あなたの Y コンビネーターは、コンパイル時のポリモーフィズムを行っています。保存するものを変更し、より統一されたインターフェイスを公開しました。

そのインターフェースと通信するものは、Y コンビネーターの内部実装の詳細を気にしません。それらを実装に含めることは、抽象化の失敗です。

evaluate呼び出し可能なものを受け取り、それを渡し、戻り値uintを期待しuintます。それが気になるところです。渡されるか関数ポインタが渡されるかはにしません。Functor<Chicken>

それを気にするのは間違いです。

を取る場合は、std::functionランタイム ポリモーフィズムを行います。template<class F>type の引数で を受け取る場合F&&、コンパイル時のポリモーフィックです。これは選択であり、それらは異なります。

あらゆる種類のを取得することFunctor<F>は、基本的に気にするべきではない API に契約要件を入れることです。

于 2021-02-05T18:09:23.043 に答える