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


98

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

アンはstd::function持つべき一定の大きさを、同じ種類のいずれかのラムダを含む、呼び出し可能オブジェクトの任意の種類を、包むことができなければなりません。どのように実装されていますか?場合はstd::function、内部では、その標的にポインタを使用し、そして何が起こるか、ときstd::functionインスタンスがコピーまたは移動されましたか?関連するヒープ割り当てはありますか?


2
std::functionしばらく前にgcc / stdlibの実装を調べました。これは基本的に、ポリモーフィックオブジェクトのハンドルクラスです。内部基底クラスの派生クラスが作成され、パラメーターを保持し、ヒープに割り当てられます。その後、これへのポインターがのサブオブジェクトとして保持されstd::functionます。std::shared_ptrコピーと移動を処理するために参照カウントを使用していると思います。
Andrew Tomazos 2013

4
実装ではマジックを使用する場合があることに注意してください。つまり、利用できないコンパイラー拡張に依存しています。これは実際にはいくつかのタイプの特性に必要です。特に、トランポリンは、標準のC ++では利用できない既知の手法です。
MSalters 2013

回答:


78

の実装は実装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になります。


@Cole "Cole9" Johnsonが自分で書いたと推測
aaronman 2013

8
@Cole "Cole9" Johnson:これは実際のコードを単純化しすぎているため、ブラウザに入力しただけなので、さまざまな理由でタイプミスやコンパイルに失敗する可能性があります。答えのコードは、型消去の実装方法/実装方法を示すためだけにあります。これは、明らかに製品品質のコードではありません。
デビッド・ロドリゲス-2013

2
@MooingDuck:ラムダはコピー可能だと思います(5.1.2 / 19)が、それは問題ではなくstd::function、内部オブジェクトがコピーされた場合のセマンティクスが正しいかどうか、そして私はそうではないと思います(キャプチャ値とが内部に格納され、変更可能であることラムダを考えるstd::function機能状態のコピー数コピーされた場合、std::function望ましくない標準的なアルゴリズムが異なる結果をもたらす可能性の内部を。
デビッドロドリゲス- dribeas

1
@MiklósHomolya:私はg ++ 4.8でテストしましたが、実装は内部状態をコピーします。呼び出し可能エンティティが動的割り当てを必要とするのに十分な大きさである場合、のコピーによりstd::function割り当てがトリガーされます。
デビッドロドリゲス-ドリベス2013

4
@DavidRodríguez-dribeas共有状態は望ましくありません。小さいオブジェクトの最適化は、コンパイラで共有状態から非共有状態に移行し、コンパイラのバージョンによってサイズのしきい値が決定されるためです(小さいオブジェクトの最適化は共有状態をブロックするため)。それは問題があるようです。
Yakk-Adam Nevraumont 2013

22

@DavidRodrí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実装は最良のソリューションではありません。しかし、根本的なメカニズムは同じだと思います。


20

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

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

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


-6

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


1
どのようにして任意の大きなラムダを固定サイズに合わせることができるのstd::functionでしょうか?それがここでの重要な質問です。
ミクローシュHomolya

2
@aaronman:すべてのstd::functionオブジェクトが同じサイズであり、含まれているラムダのサイズではないことを保証します。
Mooing Duck 2013

5
@aaronmanは、各std::vector<T...> オブジェクトが実際のアロケータインスタンス/要素数に関係なく(copiletime)固定サイズを持つのと同じ方法で。
sehe 2013

3
@aaronman:まあ、おそらくstd :: functionが任意のサイズのラムダを含むことができるように実装されている方法に答えるstackoverflow質問を見つける必要があります:P
Mooing Duck

1
@aaronman:呼び出し可能なエンティティが設定されている場合、構築、割り当て... std::function<void ()> f;そこに割り当てる必要はありませんstd::function<void ()> f = [&]() { /* captures tons of variables */ };。おそらく割り当てます。std::function<void()> f = &free_function;おそらく...どちらかの割り当てません
デビッド・ロドリゲス- dribeas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.