web-dev-qa-db-ja.com

std :: functionはどのように実装されていますか?

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

std::functionには固定サイズが必要ですが、同じ種類のラムダを含め、あらゆる種類の呼び出し可能オブジェクトをラップできる必要があります。どのように実装されていますか? std::functionは内部的にそのターゲットへのポインタを使用し、その後std::functionインスタンスがコピーまたは移動されましたか?関連するヒープ割り当てはありますか?

91
Miklós Homolya

_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_ptr_のみを格納します。 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でした。

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

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

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

19
user743382

@DavidRodríguez-dribeasからの答えは、型消去を実証するのには適していますが、型消去には型のコピー方法も含まれるため、十分ではありません(その答えでは、関数オブジェクトはコピー構築できません)。これらの動作は、ファンクターデータに加えて、functionオブジェクトにも保存されます。

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

その簡易版を作成しました。それが役立つことを願っています

#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_fおよびdestroy_fは、いくつかのバイトを節約するために(何をすべきかを指示する追加のパラメーターとともに)1つの関数ポインターに混合されます。
  • rawポインターは、unionに関数ポインターとともにファンクターオブジェクトを格納するために使用されます。そのため、functionオブジェクトが関数ポインターから構築されると、unionヒープスペースではなく

STL実装は、いくつかの 高速実装 について聞いたように、おそらく最良のソリューションではありません。しかし、基本的なメカニズムは同じだと思います。

17
neuront