111

私が見つけたソースによると、ラムダ式は基本的に、オーバーロードされた関数呼び出し演算子と参照される変数をメンバーとして持つクラスを作成するコンパイラによって実装されます。これは、ラムダ式のサイズがさまざまであることを示唆しており、サイズが任意に大きくなる可能性がある十分な参照変数が与えられていることを示しています。

Anstd::function固定サイズである必要がありますが、同じ種類のラムダを含む、あらゆる種類の callable をラップできる必要があります。それはどのように実装されていますか?ターゲットへのポインターを内部的に使用する場合std::functionstd::functionインスタンスがコピーまたは移動されるとどうなりますか? 関連するヒープ割り当てはありますか?

4

6 に答える 6

89

の実装は実装ごとにstd::function異なる場合がありますが、核となるアイデアは型消去を使用することです。それを行うには複数の方法がありますが、簡単な (最適ではない) 解決策は次のようになると想像できます (単純化のために特定のケースのstd::function<int (double)>ために単純化されています)。

struct callable_base {
   virtual int operator()(double d) = 0;
   virtual ~callable_base() {}
};
template <typename F>
struct callable : callable_base {
   F functor;
   callable(F functor) : functor(functor) {}
   virtual int operator()(double d) { return functor(d); }
};
class function_int_double {
   std::unique_ptr<callable_base> c;
public:
   template <typename F>
   function(F f) {
      c.reset(new callable<F>(f));
   }
   int operator()(double d) { return c(d); }
// ...
};

この単純なアプローチでは、functionオブジェクトはunique_ptra を基本型に格納するだけです。で使用されるそれぞれのファンクターに対してfunction、ベースから派生した新しい型が作成され、その型のオブジェクトが動的にインスタンス化されます。std::functionオブジェクトは常に同じサイズであり、必要に応じてヒープ内のさまざまなファンクターにスペースを割り当てます。

実際には、パフォーマンス上の利点を提供するさまざまな最適化がありますが、答えは複雑になります。型は小さなオブジェクトの最適化を使用できます。動的ディスパッチは、ファンクターを引数として取るフリー関数ポインターに置き換えて、1 レベルの間接化を回避できます...しかし、考え方は基本的に同じです。


のコピーがどのように動作するかという問題に関して、std::function簡単なテストでは、状態を共有するのではなく、内部呼び出し可能オブジェクトのコピーが行われることが示されています。

// g++4.8
int main() {
   int value = 5;
   typedef std::function<void()> fun;
   fun f1 = [=]() mutable { std::cout << value++ << '\n' };
   fun f2 = f1;
   f1();                    // prints 5
   fun f3 = f1;
   f2();                    // prints 5
   f3();                    // prints 6 (copy after first increment)
}

このテストは、f2が参照ではなく、呼び出し可能なエンティティのコピーを取得することを示しています。呼び出し可能なエンティティが異なるstd::function<>オブジェクトによって共有されている場合、プログラムの出力は 5、6、7 になります。

于 2013-08-26T21:29:01.910 に答える
34

@David Rodríguez からの回答 - dribeas は、型消去のデモンストレーションには適していますが、型消去には型のコピー方法も含まれているため、十分ではありません (その回答では、関数オブジェクトはコピー構築可能ではありません)。これらの動作はfunction、ファンクター データに加えて、オブジェクトにも格納されます。

Ubuntu 14.04 gcc 4.8 の STL 実装で使用されているトリックは、1 つのジェネリック関数を記述し、考えられる各ファンクター型でそれを特殊化し、それらをユニバーサル関数ポインター型にキャストすることです。したがって、タイプ情報は消去されます。

私はそれの単純化されたバージョンを石畳にしました。それが役立つことを願っています

#include <iostream>
#include <memory>

template <typename T>
class function;

template <typename R, typename... Args>
class function<R(Args...)>
{
    // function pointer types for the type-erasure behaviors
    // all these char* parameters are actually casted from some functor type
    typedef R (*invoke_fn_t)(char*, Args&&...);
    typedef void (*construct_fn_t)(char*, char*);
    typedef void (*destroy_fn_t)(char*);

    // type-aware generic functions for invoking
    // the specialization of these functions won't be capable with
    //   the above function pointer types, so we need some cast
    template <typename Functor>
    static R invoke_fn(Functor* fn, Args&&... args)
    {
        return (*fn)(std::forward<Args>(args)...);
    }

    template <typename Functor>
    static void construct_fn(Functor* construct_dst, Functor* construct_src)
    {
        // the functor type must be copy-constructible
        new (construct_dst) Functor(*construct_src);
    }

    template <typename Functor>
    static void destroy_fn(Functor* f)
    {
        f->~Functor();
    }

    // these pointers are storing behaviors
    invoke_fn_t invoke_f;
    construct_fn_t construct_f;
    destroy_fn_t destroy_f;

    // erase the type of any functor and store it into a char*
    // so the storage size should be obtained as well
    std::unique_ptr<char[]> data_ptr;
    size_t data_size;
public:
    function()
        : invoke_f(nullptr)
        , construct_f(nullptr)
        , destroy_f(nullptr)
        , data_ptr(nullptr)
        , data_size(0)
    {}

    // construct from any functor type
    template <typename Functor>
    function(Functor f)
        // specialize functions and erase their type info by casting
        : invoke_f(reinterpret_cast<invoke_fn_t>(invoke_fn<Functor>))
        , construct_f(reinterpret_cast<construct_fn_t>(construct_fn<Functor>))
        , destroy_f(reinterpret_cast<destroy_fn_t>(destroy_fn<Functor>))
        , data_ptr(new char[sizeof(Functor)])
        , data_size(sizeof(Functor))
    {
        // copy the functor to internal storage
        this->construct_f(this->data_ptr.get(), reinterpret_cast<char*>(&f));
    }

    // copy constructor
    function(function const& rhs)
        : invoke_f(rhs.invoke_f)
        , construct_f(rhs.construct_f)
        , destroy_f(rhs.destroy_f)
        , data_size(rhs.data_size)
    {
        if (this->invoke_f) {
            // when the source is not a null function, copy its internal functor
            this->data_ptr.reset(new char[this->data_size]);
            this->construct_f(this->data_ptr.get(), rhs.data_ptr.get());
        }
    }

    ~function()
    {
        if (data_ptr != nullptr) {
            this->destroy_f(this->data_ptr.get());
        }
    }

    // other constructors, from nullptr, from function pointers

    R operator()(Args&&... args)
    {
        return this->invoke_f(this->data_ptr.get(), std::forward<Args>(args)...);
    }
};

// examples
int main()
{
    int i = 0;
    auto fn = [i](std::string const& s) mutable
    {
        std::cout << ++i << ". " << s << std::endl;
    };
    fn("first");                                   // 1. first
    fn("second");                                  // 2. second

    // construct from lambda
    ::function<void(std::string const&)> f(fn);
    f("third");                                    // 3. third

    // copy from another function
    ::function<void(std::string const&)> g(f);
    f("forth - f");                                // 4. forth - f
    g("forth - g");                                // 4. forth - g

    // capture and copy non-trivial types like std::string
    std::string x("xxxx");
    ::function<void()> h([x]() { std::cout << x << std::endl; });
    h();

    ::function<void()> k(h);
    k();
    return 0;
}

STLバージョンにもいくつかの最適化があります

  • 数バイトを節約するために、construct_fdestroy_fが 1 つの関数ポインターに混合されます (何をすべきかを指示する追加のパラメーターを使用)。
  • 生のポインターは、関数ポインターと共にファンクター オブジェクトを格納するために使用されるunionため、オブジェクトが関数ポインターから構築されると、ヒープ領域ではなくfunction直接格納されます。union

より高速な実装について聞いたことがあるので、STL 実装は最善の解決策ではないかもしれません。しかし、根底にあるメカニズムは同じだと思います。

于 2016-07-20T09:56:25.093 に答える
20

特定のタイプの引数 (「 f のターゲットが、reference_wrapperまたは関数ポインターを介して渡された呼び出し可能オブジェクトである場合」)std::functionの場合、コンストラクターは例外を許可しないため、動的メモリの使用は問題外です。この場合、すべてのデータをstd::functionオブジェクト内に直接格納する必要があります。

一般的なケース (ラムダのケースを含む) では、動的メモリの使用 (標準のアロケーター、またはstd::functionコンストラクターに渡されたアロケーターのいずれかを介して) は、実装が適切であると判断した場合に許可されます。標準では、回避できる場合は実装が動的メモリを使用しないことを推奨していますが、当然のことながら、関数オブジェクト (std::functionオブジェクトではなく、その中にラップされているオブジェクト) が十分に大きい場合、それを防ぐ方法はありません。std::function固定サイズなので。

例外をスローするこの許可は、通常のコンストラクターとコピー コンストラクターの両方に付与されます。これにより、コピー中の動的メモリ割り当てもかなり明示的に許可されます。移動の場合、動的メモリが必要になる理由はありません。標準はそれを明示的に禁止していないようであり、移動がラップされたオブジェクトの型の移動コンストラクターを呼び出す可能性がある場合はおそらく禁止されていませんが、実装とオブジェクトの両方が適切である場合、移動によって問題が発生しないと想定できるはずです。任意の割り当て。

于 2013-08-26T21:30:18.327 に答える
-5

Anはそれをファンクター オブジェクトにすることをstd::functionオーバーロードします。ラムダも同じように機能します。operator()基本的に、関数内でアクセスできるメンバー変数を持つ構造体を作成しoperator()ます。したがって、覚えておくべき基本的な概念は、ラムダは関数ではなくオブジェクト (ファンクターまたは関数オブジェクトと呼ばれる) であるということです。標準では、回避できる場合は動的メモリを使用しないように規定されています。

于 2013-08-26T21:26:06.443 に答える