🔄

std::functionを自作Functionに置き換え(動的確保を排除)

2023/08/02に公開

はじめに

動的確保をしないstd::functionの実装についての話です。C++でデリゲートを実装という記事の延長線の話でstd::functionに依存していた部分を自作のfunctionに置き換えることが目的です。

前回のコード
#include <functional>

template<typename T>
class Delegate;

template<typename ReturnT, typename... Args>
class Delegate<ReturnT(Args...)>
{
public:
    using FuncT = std::function<ReturnT(Args...)>;

    template<typename Class>
    void Bind(Class* instance, ReturnT(Class::* memFn)(Args...))
    {
        function = [instance, memFn](Args... args) -> ReturnT {
            return (instance->*memFn)(args...);
        };
    }

    void Bind(FuncT fn) {
        function = fn;
    }

    ReturnT Execute(Args... args) {
        return function(args...);
    }

    void UnBind() {
        function = nullptr;
    }

private:

    FuncT function;
};

std::functionを置き換えるとは?

前回の記事では通常のstd::functionはメンバ関数のバインドが特に面倒ということで、std::functionをラップして自分の好みのフォーマットで関数をバインドできるようにするというのが目的であり、以下の様にシンプルなフォーマットにすることが出来ました。

Delegate<void(int, int)> func;
func.Bind(this, &Class::Func);
func.Excute(1, 2);

実装は単純にstd::functionをラップして可変長パラメータを渡すという実装です。
今回は、前回のインターフェースはそのままにstd::function 部分を自前の Functionに置き換えようというこころみです。

// std::functionを可変長パラメータで型エイリアス
using TFunction = std::function<TReturn(Args...)>;

// 自前のfunctionに置き換え
using TFunction = Function<TReturn(Args...)>;

なぜ置き換えたいのか?

置き換えたい理由は、タイトルにもあるように std::functionが動的確保をするのを避けたい からです。std::functionはサイズの小さい関数オブジェクトであれば静的領域を使って動的確保をしない最適化(SBO: Small Buffer Optimization)が行われます。当然ですが、常に静的領に割り当てるようにすれば動的確保しないFunctionが作れます。

また、静的バッファを最小サイズ(8バイト)にすれば、最大でstd::functionの半分のサイズにすることができます。しかし、動的にバッファを変更できないので動的な関数オブジェクトサイズの変動に弱く、Functionの場合は宣言時のサイズ以上の関数オブジェクトは再バインドできません。逆にstd::functionは任意サイズの関数オブジェクトの再バインドが可能です。

sizeof(Function<void(), 8>);   // 32 byte
sizeof(std::function<void()>); // 64 byte

実装

前回のコード

前回のコードに変更を加えながら実装していく

Delegate.h
#include <functional>

template<typename T>
class Delegate;

template<typename ReturnT, typename... Args>
class Delegate<ReturnT(Args...)>
{
public:
    using FuncT = std::function<ReturnT(Args...)>;

    template<typename Class>
    void Bind(Class* instance, ReturnT(Class::* memFn)(Args...))
    {
        function = [instance, memFn](Args... args) -> ReturnT {
            return (instance->*memFn)(args...);
        };
    }

    void Bind(FuncT fn) {
        function = fn;
    }

    ReturnT Execute(Args... args) {
        return function(args...);
    }

    void UnBind() {
        function = nullptr;
    }

private:

    FuncT function;
};

関数ポインタに置き換え

まず、std::functionを使っていた部分を取り除き、関数ポインタに置き換えてみると...

//using FuncT = std::function<ReturnT(Args...)>;
using FuncT = ReturnT(*)(Args...);

通常関数であれば渡すことができますが、メンバ関数は渡すことができなくなります。

Delegate<void()> d;
d.Bind(&Hoge);            // 〇
d.Bind(this, &Foo::Func); // ✕
// Delegate<void(void)>::Bind::<lambda_1>' から 'void (*)(void)' に変換できません

引数自体は正しく渡せていますが、エラーが起きてしまいました。原因はvoid (*)(void)という関数ポインタに、ラムダ式を代入しようとしているからです。

メンバ関数とラムダ式

メンバ関数はinstance.Func()のように呼び出しにはインスタンスが必要です。なので、メンバ関数を呼び出す場合はinstance.Func()という処理を行うラムダ式を代入し、それを呼び出すという方法をとる必要があります。ラムダ式は関数ポインタではなく、ラムダ式の内容を実行するoperator()を持った型であり、ラムダ式は関数オブジェクトです。だから関数ポインタに対して値型を渡したことがエラーになったというわけです。

template<typename Class>
void Bind(Class* instance, ReturnT(Class::* memFn)(Args...))
{
    // ポインターに対する値型の代入
    function = [instance, memFn](Args... args) -> ReturnT {
        return (instance->*memFn)(args...);
    };
}

関数呼び出し可能な型であればなんでも

functionにメンバ関数も保存するのであれば、functionは関数オブジェクト(ラムダ式)を代入可能な型に変える必要があります。なのでテンプレートパラメータT型として保存する必要がありそうです。また、functionに対する関数呼び出し演算子()で関数が呼ばれるための条件は、T型が関数ポインターであるか、operator()を実装している関数オブジェクトのいずれかである必要があるわけです。つまるところT型はどんな型かわからないけど関数呼び出しが出来る型であって欲しいわけです。

// 関数オブジェクト
struct FO { void operator()(void){} };
FO obj;

// 関数ポインタ
using FP = void(*)(void);
FP ptr = &Func;

// どちらも関数呼び出し可能な型
obj();
ptr();

FO型FP型Tとしたクラスを実装すればなんとなく実装出来そう?

使うまで型がわからない

実際にテンプレート化してみればわかりますが、上手くいきません。これはBind関数を呼び出すまでT型の詳細がわからないからです。これはジレンマです。型の詳細が分からないからパラメータTとして隠蔽したのに、Tが宣言時には判明しないのでクラステンプレートパラメータとして公開できず、インスタンス化できない...

template <typename Signature>
class Function;

template <typename ReturnT, typename... Args>
struct Function<ReturnT(Args...)>
{
    // 今はわからないけどF型なんだな...
    template <typename F>
    void Bind(F&& f) {
        callable = F(std::forward<F>(f));
    }
    
    // パラメータにないけど、Tは何型?
    T callable;
};

ポリモーフィズムと型消去(Type Erase)

こんな時、具体的な型データは分からないけど、共通のインターフェース呼び出しだけで実際の型データにあった呼び分けを行う方法がありました。そうです、多態性(ポリモーフィズム)です。前述したように、インターフェースクラスはどんな型かわからないけど関数呼び出しが出来る型が欲しいので、operator()を実装するように強制するICallableクラスを作ります。

// どんな型かわからないけど関数呼び出しが出来る型
struct ICallable
{
    virtual void operator()() const = 0;
};

次は実際の処理を行う派生先です。従来のポリモーフィズムはこの派生クラスを型データ分用意して各々実装しますが、このポリモーフィズムはテンプレートを使うので派生型は1つしか使用しません。T型のインスタンス化はテンプレートが担当し、T型の実際の内容は外部からもらいます。

// T型に対してoperator() を実行する
template <typename T>
struct Callable : public ICallable
{
    // 関数ポインタ or 関数オブジェクト
    T functor;
    
    // コンストラクタ
    Callable(T&& func) : functor(std::forward<T>(func)){}
    
    // Tに対する関数呼び出し or operator() 呼び出し
    void operator()() const override { return functor(); }
};

実際のコードとは異なりますが、インスタンス化はこのようなイメージです。

ICallable* callable = nullptr;

template <typename F>
void Bind(F&& func) {
    callable = new Callable<T>(std::forward<F>(func));
}

ICallableはインターフェースであり、具体的な型データを必要としません。つまり、テンプレートパラメータが消せるということです。これで関数をバインドするまで実際の型データがわからないという問題が解決しました。

継承を使ったポリモーフィズムは、インターフェースとなる特定の型を継承することで「インターフェースを介した操作」が可能になるのに対して、ICallableを使ったポリモーフィズムはインターフェースがFunctionクラス内に隠蔽されており、特定のインターフェースを継承しなくても、実行時に渡されたオブジェクトに対して共通のインターフェース操作が可能になります(もちろん、インターフェースが求める操作を実装している必要はあります)

ポリモーフィズムは その型がインターフェースが求める共通の処理を実装している ということを保証する機能でもあります。その点、Functionクラスは、Bind関数で渡されるまで具体的な型として扱わず、どんな型かわからないけど関数呼び出しが出来る型として扱っています。この様に、実行時まで具体的な型を定義ぜず消してしまうことを型消去といいます。

ここまでの実装

仮ではありますが、現状の全体コードの確認です。現在は全ての関数がバインド可能(メンバ関数のバインドは少し工夫が必要)という状態です。ただし、動的確保をおこなっている部分があるので、そこを静的領域への割り当てを行うように変更していきたいと思います。

現在の仮コード
#include <type_traits>

template <typename Signature>
class Function;

template <typename ReturnT, typename... Args>
class Function<ReturnT(Args...)>
{
public:

    Function() = default;
    ~Function() { Unbind(); }

public:

    template <typename F>
    void Bind(F&& f)
    {
        callable = new Callable<F>(std::forward<F>(f));
    }

    void Unbind()
    {
        if (callable)
        {
            delete callable;
            callable = nullptr;
        }
    }

private:

    struct ICallable
    {
        virtual ~ICallable() = default;
        virtual ReturnT operator()(Args...) const = 0;
    };

    template <typename T>
    struct Callable : public ICallable
    {
        T functor;

        Callable(T&& f) : functor(std::forward<T>(f)) {}

        ReturnT operator()(Args... args) const override
	{ 
	    return functor(std::forward<Args>(args)...);
	}
    };

private:

    ICallable* callable = nullptr;
};

動的確保を無くしていく

あとは動的確保を無くすだけです。newの動的確保をplacement newにかえてクラス内の静的バッファにオブジェクトを構築するようにします。

callable = new (buffer) Callable<F>(std::forward<F>(f));

バッファサイズはテンプレート引数で指定するようにします。任意でデフォルト引数を追加しても良いでしょう。メンバ関数のバインドを考えると最小で16バイト必要なので16にします。

template <typename Signature, std::size_t BufferSize = 16>
class Function;

template <typename ReturnT, typename... Args, std::size_t BufferSize>
class Function<ReturnT(Args...), BufferSize>
{
    //...
    uint8 buffer[BufferSize] = { 0 };
}

それにともなってUnbind関数内も静的確保用に変更します。deleteからデストラクタ呼び出しに変え、メモリを0クリアします。

void Unbind()
{
    if (callable)
    {
        callable->~ICallable();
        callable = nullptr;
    }

    std::memset(buffer, 0, BufferSize + sizeof(void*));
}

呼び出し部分も追加します。インターフェースに対して関数呼び出し()を行うだけで、特別なことはしていません。callableどんな型かわからないけど関数呼び出しが出来る型です。関数ポインタであれば関数呼び出し()になり、関数オブジェクトならoperator()が呼ばれます。

ReturnT execute(Args... args)
{
    return (*callable)(std::forward<Args>(args)...);
}

完成

これで完成です。本家のstd::functionに到底及びませんが、使えないことはないかなと思います。全体コードでは追加処理や、前回のDelegateMulticastDelegateも実装し直しているのでよかったら参考にしてください。

全体コード

https://gist.github.com/Suuta/43b2e7ee973d8d3fda581e89efe28b45
https://gist.github.com/Suuta/e294208c04f346103723a9df5e134d2f
https://gist.github.com/Suuta/d15f8940c2a4d7301020dceac9c51e61

Discussion