C++ラムダ式の関数シグネチャを得る

INDEX

はじめに

こんにちは。モノリスソフト プログラマーの鈴木です。
C++11から使えるようになりましたラムダ式はとても便利で、ゲーム開発の現場でもよく活用されていると思います。
std::functionと組み合わせて使うと、お手軽に使えてとても便利ですよね。

この記事ではラムダ式の返り値引数の型といった、関数シグネチャをコンパイル時に得る方法を紹介します。

この関数シグネチャが得られると何が嬉しいかというと、例えば

  • C++のラムダ式を別のスクリプトから関数として呼び出す
  • リモートで動いているプログラムのラムダ式をRPCで呼び出す

といったことを行いたいときに役立ちます。

これらは関数呼び出し自体の情報を一旦シリアライズしないといけません。
手作業でシリアライズ処理を書いてもいいのですが、C++のテンプレートを活用してコンパイル時に自動生成できれば、効率化ができミスも減らせそうです。
このテクニックはPython向けにC++をバインドするときによく使われるpybind11などで使われています。

手順

ラムダ式を書く

まずこのようなラムダ式を書きます。

// このラムダ式のシグネチャが欲しい!
auto myLambda = [](int command, std::string name, Vec3 position){
    return "hello";
};

ラムダ式の型を得る

次にdecltypeでラムダ式自体の型を得ます。

// ラムダ式の型
using MyLambdaType = decltype(myLambda);
// 型情報を見てみる
std::cout << typeid(MyLambdaType).name() << std::endl;

次の結果が出力されました。

class <lambda_43e1bc3529f0ae8ecb4dc9c03a226adb> 

ラムダ式の型の実態は自動生成されたクラスなんですね。

ラムダ式を返却型と引数型に分解する

メンバ関数ポインタを「クラス」と「返却の型」と「引数の型」に分解する可変引数テンプレートを作成します。

// 関数ヘルパー構造体の前方宣言
template <class>
struct FuncHelper;
 
// 通常のラムダ式(constなメンバ関数)の関数ヘルパー
template <class TResult, class TClass, class... TArgs>
struct FuncHelper<TResult(TClass::*)(TArgs...) const>
{
    static std::array<std::string, 1 + sizeof...(TArgs)> signatureNames()
    {
        return { typeid(TResult).name(), typeid(TArgs).name()... };
    }
};

どうしてここにメンバ関数ポインタが出てくるかというと、C++のラムダ式は関数オブジェクトの糖衣構文であるため、メンバ関数であるoperator()があるからです。

operator()の型をテンプレートパラメータとして渡せば、テンプレートで戻り値と引数の型に分解することができるのです。

それでは::operator()をdecltypeで包んでFuncHelperテンプレートに指定してみましょう。

// ラムダ式の型ヘルパー
using MyLambdaHelper = FuncHelper<decltype(&MyLambdaType::operator())>;

結果を確認する

staticメンバ関数のsignatureNames()を呼び出すと、戻り値と引数の情報が入った固定長配列が返るので出力してみます。

// シグネチャ情報を出力する
for (auto name : MyLambdaHelper::signatureNames())
{
    std::cout << name << std::endl;
}

上記を実行すると次の結果になりました。

char const *
int
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
struct Vec3
  • 1行目: ラムダ式の返り値の型を出力したものです。
    • char const * は return "hello"; を型推論した内容ですね。
  • 2~4行目: ラムダ式の引数の型を左から順番に出力したものです。
    • 3行目は第2引数の std::string の正式な型名になります。

以上で関数シグネチャが得られていることが分かりました。

プログラムコード

#include <iostream>
#include <string>
#include <array>
 
// 関数ヘルパー構造体の前方宣言
template <class>
struct FuncHelper;
 
// 通常のラムダ式(constなメンバ関数)の関数ヘルパー
template <class TResult, class TClass, class... TArgs>
struct FuncHelper<TResult(TClass::*)(TArgs...) const>
{
    static std::array<std::string, 1 + sizeof...(TArgs)> signatureNames()
    {
        return { typeid(TResult).name(), typeid(TArgs).name()... };
    }
};
 
// mutableなラムダ式(通常のメンバ関数)の関数ヘルパー
template <class TResult, class TClass, class... TArgs>
struct FuncHelper<TResult(TClass::*)(TArgs...)>
{
    static std::array<std::string, 1 + sizeof...(TArgs)> signatureNames()
    {
        return { typeid(TResult).name(), typeid(TArgs).name()... };
    }
};
 
//----------------------------------------------------------------
// 以下サンプルコード
//----------------------------------------------------------------
 
// 独自の型
struct Vec3
{
    float x, y, z;
};
 
int main()
{
    // このラムダ式のシグネチャが欲しい!
    auto myLambda = [](int a, std::string b, Vec3 c){ return "hello"; };
 
    // ラムダ式の型
    using MyLambdaType = decltype(myLambda);
 
    // ラムダ式の型ヘルパー
    using MyLambdaHelper = FuncHelper<decltype(&MyLambdaType::operator())>;
 
    // シグネチャ情報を出力する
    for (auto name : MyLambdaHelper::signatureNames())
    {
        std::cout << name << std::endl;
    }
 
    return 0;
}

応用例

応用例として引数を文字列にシリアライズして、その文字列を使ってラムダ式を呼び出すコードを載せます。

#include <iostream>
#include <sstream>
#include <string>
#include <array>
#include <tuple>
#include <utility>
 
// シリアライズ用
struct Writer
{
    std::ostringstream ss;
     
    void flush()
    {
        ss << std::endl;
    }
    std::string getData()
    {
        return ss.str();
    }
    void writeInt(int value)
    {
        ss << value << " ";
    }
    void writeFloat(float value)
    {
        ss << value << " ";
    }
    void writeString(const std::string& value)
    {
        ss << value << " ";
    }
};
 
// デシリアライズ用
struct Reader
{
    std::istringstream ss;
     
    Reader(const std::string& str)
        : ss(str)
    {
    }
    int readInt()
    {
        int value;
        ss >> value;
        return value;
    }
    float readFloat()
    {
        float value;
        ss >> value;
        return value;
    }
    std::string readString()
    {
        std::string value;
        ss >> value;
        return value;
    }
};
 
// 独自構造体
struct Vec3
{
    float x, y, z;
};
 
// 型ヘルパー
template <class T, class = void>
struct TypeHelper;
 
// 型ヘルパーのint特殊化
template <class T>
struct TypeHelper<T, typename std::enable_if<std::is_integral<T>::value>::type>
{
    static void pack(Writer& writer, int value)
    {
        writer.writeInt((int)value);
    }
    static int unpack(Reader& reader)
    {
        return reader.readInt();
    }
};
 
// 型ヘルパーstd::string特殊化
template <>
struct TypeHelper<std::string>
{
    static void pack(Writer& writer, std::string value)
    {
        writer.writeString(value);
    }
    static std::string unpack(Reader& reader)
    {
        return reader.readString();
    }
};
 
// 型ヘルパーVec3特殊化
template <>
struct TypeHelper<Vec3>
{
    static void pack(Writer& writer, Vec3 value)
    {
        writer.writeFloat(value.x);
        writer.writeFloat(value.y);
        writer.writeFloat(value.z);
    }
    static Vec3 unpack(Reader& reader)
    {
        Vec3 v;
        v.x = reader.readFloat();
        v.y = reader.readFloat();
        v.z = reader.readFloat();
        return v;
    }
};
 
// 関数ヘルパー構造体の前方宣言
template <class>
struct FuncHelper;
 
// ラムダ式の関数ヘルパー
template <class TResult, class TClass, class... TArgs>
struct FuncHelper<TResult(TClass::*)(TArgs...) const>
{
    using Result = TResult;
    using ArgsTuple = std::tuple<TArgs...>;
 
    static std::array<std::string, 1 + sizeof...(TArgs)> signatureNames()
    {
        return { typeid(TResult).name(), typeid(TArgs).name()... };
    }
 
    static void packArgs(Writer& writer, TArgs... args)
    {
        (TypeHelper<TArgs>::pack(writer, std::forward<TArgs>(args)), ...);
    }
 
    static ArgsTuple unpackArgs(Reader& reader)
    {
        return unpackArgsImpl(reader, std::make_index_sequence<sizeof...(TArgs)>());
    }
 
    template <class Func>
    static Result invoke(Func&& func, Reader& reader)
    {
        return std::apply(func, unpackArgsImpl(reader, std::make_index_sequence<sizeof...(TArgs)>()));
    }
 
private:
    template <size_t... Index>
    static ArgsTuple unpackArgsImpl(Reader& reader, std::index_sequence<Index...>)
    {
        ArgsTuple args;
        ((std::get<Index>(args) = TypeHelper<typename std::tuple_element<Index, ArgsTuple>::type>::unpack(reader)), ...);
        return args;
    }
};
 
int main()
{
    // ラムダ式
    auto myLambda = [](int number, std::string name, Vec3 position){
        std::stringstream ss;
        ss << "number=" << number << " name=" << name << " pos=(" << position.x << ", " << position.y << ", " << position.z << ")";
        return ss.str();
    };
 
    // ラムダ式のシグネチャ宣言
    using MyLambdaType = decltype(myLambda);
    using MyLambdaHelper = FuncHelper<decltype(&MyLambdaType::operator())>;
 
    // シリアライズしたデータをライターに出力
    Writer writer;
    MyLambdaHelper::packArgs(writer, 5, "alice", Vec3{1, 2, 3});
    writer.flush();
 
    // シリアライズ結果を表示
    std::string data = writer.getData();
    std::cout << "シリアライズデータ: " << data;
 
    // リーダーから入力したデータをデシリアライズしてラムダ式を呼び出す
    Reader reader(data);
    auto result = MyLambdaHelper::invoke(myLambda, reader);
     
    // ラムダ式の結果を表示
    std::cout << "ラムダ式の返却データ: " << result << std::endl;
 
    return 0;
}

上記を実行すると次の結果になります。

シリアライズデータ: 5 alice 1 2 3
ラムダ式の返却データ: number=5 name=alice pos=(1, 2, 3)

これだけだと特に意味のあるプログラムではありませんが、シリアライズした後に通信を挟むとリモート環境先のラムダ式の呼び出しが行えたりします。

シリアライザにJSONやMessagePackなど使用してもいいと思います。

まとめ

  • C++のラムダ式は関数オブジェクトの糖衣構文なので、operator()メンバ関数が存在する
  • メンバ関数は可変引数テンプレートでクラス、引数型、返却型に分離できる
  • それらの型についてシリアライザを作ると、関数コール自体をシリアライズできる
  • 上記を活用してスクリプトのバインダ、RPCなどに応用できる

ラムダ式の関数シグネチャを得る方法と、その応用例を解説しました。

昔のC++は黒魔術的なテンプレートテクニックを駆使してメタプログラミングを行っていたと記憶しています。しかし近年C++のバージョンが上がるにつれ比較的綺麗なコードで強力なメタプログラミングが行えるようになってきたように感じます。

もし本記事が皆さんのお役に立てれば幸いです。

参考

cpprefjp - ラムダ式
https://cpprefjp.github.io/lang/cpp11/lambda_expressions.html

cpprefjp - 可変引数テンプレート
https://cpprefjp.github.io/lang/cpp11/variadic_templates.html

GitHub - pybind/pybind11
https://github.com/pybind/pybind11

執筆者:鈴木

ツールミドルウェアの会社を経てモノリスソフトへ入社。以来、プログラマーとして主に開発支援ツール関連の業務を担当。好きな焼き鳥はつくね。

ABOUT

モノリスソフト開発スタッフが日々取り組んでいる技術研究やノウハウをご紹介

RECRUIT採用情報