web-dev-qa-db-ja.com

C ++ 14の再帰ラムダ関数

C++ 11で再帰的なラムダ関数を作成するには、次のように頻繁に繰り返される「トリック」があります。

std::function<int(int)> factorial;
factorial = [&factorial](int n)
{ return n < 2 ? 1 : n * factorial(n - 1); };

assert( factorial(5) == 120 );

(例: C++ 0xの再帰ラムダ関数 。)

ただし、この手法には2つの直接的な欠点があります。std::function<Sig>オブジェクトのターゲットが(参照によるキャプチャを介して)非常に特定のstd::function<Sig>オブジェクト(ここではfactorial)に関連付けられています。これは、結果のファンクターは通常、関数から返すことができないことを意味します。そうしないと、参照がぶら下がったままになります。

もう1つの(それほど差し迫ったものではありませんが)問題は、std::functionを使用すると、通常、コンパイラの最適化が妨げられることです。これは、実装で型消去が必要になるという副作用です。これは架空のものではなく、簡単にテストできます。

再帰的なラムダ式が本当に便利であるという仮定の状況で、それらの問題に対処する方法はありますか?

46
Luc Danton

問題の核心は、C++ラムダ式では、implicitthisパラメーターが常に次の囲んでいるコンテキストのオブジェクトを参照することです。式が存在する場合は、ラムダ式の結果であるファンクターオブジェクトではありません。

匿名再帰 (「オープン再帰」とも呼ばれます)からリーフを借用すると、C++ 14の一般的なラムダ式を使用してを再導入できます。再帰関数を参照するexplicitパラメーター:

_auto f = [](auto&& self, int n) -> int
{ return n < 2 ? 1 : n * self(/* hold on */); };
_

発信者には、次の形式の電話をかけるという新しい負担があります。 f(f, 5)。ラムダ式は自己参照であるため、実際にはそれ自体の呼び出し元であり、したがってreturn n < 2 ? 1 : n * self(self, n - 1);が必要です。

ファンクターオブジェクト自体を最初の位置に明示的に渡すパターンは予測可能であるため、この醜い疣贅をリファクタリングできます。

_template<typename Functor>
struct fix_type {
    Functor functor;

    template<typename... Args>
    decltype(auto) operator()(Args&&... args) const&
    { return functor(functor, std::forward<Args>(args)...); }

    /* other cv- and ref-qualified overloads of operator() omitted for brevity */
};

template<typename Functor>
fix_type<typename std::decay<Functor>::type> fix(Functor&& functor)
{ return { std::forward<Functor>(functor) }; }
_

これにより、次のように書くことができます。

_auto factorial = fix([](auto&& self, int n) -> int
{ return n < 2 ? 1 : n * self(self, n - 1); });

assert( factorial(5) == 120 );
_

成功しましたか? _fix_type<F>_オブジェクトには、呼び出しごとに渡される独自のファンクターが含まれているため、参照がぶら下がるリスクはありません。したがって、私たちのfactorialオブジェクトは、面倒なことなく、本当に無限にコピーしたり、関数から出し入れしたりすることができます。

ただし、「外部」の呼び出し元はfactorial(5)の形式で簡単に呼び出しを行うことができますが、ラムダ式の内部では、再帰呼び出しはself(self, /* actual interesting args */)のように見えます。 _fix_type_を変更してfunctorをそれ自体に渡さず、代わりに_*this_を渡すことで、これを改善できます。つまり、最初の位置に正しい 'implicit-as-explicit'引数を渡すことを担当する_fix_type_オブジェクトを渡します:return functor(*this, std::forward<Args>(args)...);。すると、再帰はn * self(n - 1)になります。

最後に、これは、アサーションの代わりにreturn factorial(5);を使用するmainに対して生成されたコードです(_fix_type_のいずれかのフレーバーに対して):

_00000000004005e0 <main>:
  4005e0:       b8 78 00 00 00          mov    eax,0x78
  4005e5:       c3                      ret    
  4005e6:       66 90                   xchg   ax,ax
_

コンパイラーは、一般的な再帰関数を使用した場合と同様に、すべてを最適化することができました。


費用はいくらですか?

鋭敏な読者は、1つの奇妙な詳細に気づいたかもしれません。非ジェネリックからジェネリックラムダへの移行で、明示的な戻り値の型(つまり、_-> int_)を追加しました。どうして?

これは、推定される戻り値の型が条件式の型であるという事実と関係があります。この型は、推定されるselfの呼び出しに依存します。 通常の関数の戻り値の型の推定 をざっと読むと、ラムダ式を次のように書き直すことが機能するはずです。

_[](auto&& self, int n)
{
    if(n < 2) return 1;               // return type is deduced here
    else return n * self(/* args */); // this has no impact
}
_

GCCは実際、このコードを最初の形式の_fix_type_のみ(functorを渡すもの)で受け入れます。他の形式(_*this_が渡される場所)について文句を言うのが正しいかどうかを判断できません。タイプの推測を減らすか、醜い再帰呼び出しを減らすかというトレードオフを選択するのは読者に任せます(もちろん、どちらのフレーバーにも完全にアクセスすることは可能です)。


GCC4.9の例

62
Luc Danton

これはラムダ式ではありませんが、コードはほとんどなく、C++ 98で動作し、can recurse:

struct {
    int operator()(int n) const {
        return n < 2 ? 1 : n * (*this)(n-1);
    }
} fact;
return fact(5);

による [class.local]/1、それは、囲んでいる関数がアクセスできるすべての名前にアクセスできます。これは、メンバー関数のプライベート名にとって重要です。

もちろん、ラムダではないため、関数オブジェクトの外部の状態をキャプチャする場合は、コンストラクターを作成する必要があります。

14