LoginSignup
32
14

More than 1 year has passed since last update.

【闇】C++でユーザー定義属性風のことをしている話

Last updated at Posted at 2021-09-23

はじめに

まず重要なことですが、C++にはユーザー定義属性を作る機能はありません。

今回紹介するのは、ユーザー定義属性「」であり、属性ではありません。
半分くらいネタ記事だと思ってもらってよいです。

モチベーション

もともと、外部データを構造体に変換するコードを書くのが面倒くさいなぁという気持ちから始まりました。

イメージ
struct User
{
    int id;
    std::string name;
    int age;
};

// データベースのデータから変換
// こーゆー関数を書くのが面倒くさいなぁ
User FromDBRow(const DBRow& row)
{
    User ret{};
    ret.id   = row["id"].get<int>();
    ret.name = row["name"].get<std::string>();
    ret.age  = row["age"].get<int>();
    return ret;
}

(いきなり、DBRowクラスとかが登場しましたが、驚く必要はありません。私がいま適当に妄想したクラスです。)

今回の記事では、こちらの例を題材にして実装を進めていきます。

ユーザー定義属性みたいな機能がC++にあれば、よしなに紐づけれて、変換のコードを自動化できそうですが…

イメージ
struct User
{
    [[DBColumn("id")]]
    int id;

    [[DBColumn("name")]]
    std::string name;

    [[DBColumn("age")]]
    int age;
};

まぁできないものは、できません。

でも、属性「風」ならそれっぽいことができるかも…!

属性「風」とは

ポイント

C++の属性は空の属性を書いても無視されるだけなので以下みたいなコードを書いても問題ありません。
ちなみに、中に何か存在しない属性を書いても無視されてコンパイルは通りますが、警告がでます。

struct User
{
    // OK ?
    [[]]
    int id;

    [[]][[]] // 2つあってもOK??
    std::string name;

    // Warning
    [[DBColumn("age")]]
    int age;    
};

ここで以下のようなマクロをつくります。

#define DB_COLUMN(x) ]][[

もちろんこれだけは何の意味もありませんが、これを使うと以下みたいにできます。

#define DB_COLUMN(x) ]][[

struct User
{
    [[DB_COLUMN("id")]] // [[]][[]]に展開
    int id;

    [[DB_COLUMN("name")]]
    std::string name;

    [[DB_COLUMN("age")]]
    int age;    
};

見た目は属性っぽさを維持できています。
この記事における属性「風」とは、このテクニックのことを指します。

#define ATTRIBUTE ]] someting [[

この記事における、ユーザー定義属性っぽさは、このマクロ前後の空白属性がすべてです。()

見た目を属性っぽくするためだけに、マクロに]][[を含めてるだけなので、そこをやめれば
以下でも同じになります。

#define DB_COLUMN(x) 

struct User
{
    DB_COLUMN("id")
    int id;

    DB_COLUMN("name")
    std::string name;

    DB_COLUMN("age")
    int age;    
};

なんとなく属性構文っぽく[[]]に囲まれてるほうが雰囲気がいいなくらいのお気持ちです。

今後この、間に挟んだDB_COLUMNなどのマクロにいろいろ処理をいれていく事になります。

便利マクロの実装をしていく

さきほどユーザー定義属性風のことしたのはいいですが、当初やりたかったことは何もできてないので
これから間に挟んだマクロを強化していきます。

これから闇みたいなテクニックを使っていきます…!

変数(関数)との紐づけを考える

なんちゃって属性なので、直後の変数などと関係がまったくないので、マクロ引数での紐づけが必須になります。

    // No 属性ではないのでuserIdと"user_id"は関連つけようがない
    [[DB_COLUMN("user_id")]] 
    int userId;
----------------------------------------------
    // OK
    [[DB_COLUMN(userId, "user_id")]] 
    int userId;   
----------------------------------------------
    // OK 関連させたいものが変数名から判断できる
    [[DB_COLUMN(userId)]] 
    int userId; 

マクロの展開を考える

どのように展開されたらやりたいことができるか考えます。
今回の記事の例だとDBのキーと変数を紐づけたかったので
たとえば以下のようにしたらどうでしょうか?
(もう属性風の話は終わったので[[]]は省略してます。)

#define DB_COLUMN(key) \
void set_##key(const DBRow& row)\
{\
    key = row[#key].get<decltype(key)>();\
}
展開後
struct User
{
    void set_id(const DBRow& row)
    {
        id = row["id"].get<decltype(id)>();
    }
    int id; 

    void set_name(const DBRow& row)
    {
        name = row["name"].get<decltype(name)>();
    }    
    std::string name;

    void set_age(const DBRow& row)
    {
        age = row["age"].get<decltype(age)>();
    }      
    int age;    
};

それっぽくなりましたが、一つ大きな問題があります。
どうやって、これらのセッターを全部呼び出すのでしょうか?

むずかしそうです。

シーケンス作戦

いきなり答えから言いますが、__LINE__ を使って 行数番号をタグとして埋め込みます。
そうすれば、整数をメタ情報として付与できるので、メタプログラミングでぶん回しできます。

template<size_t Line>
struct tag{};
#define DB_COLUMN(key) \
void set_value_from_row(const DBRow& row, tag<__LINE__>)\
{\
    key = row[#key].get<decltype(key)>();\
}
展開後
struct User
{
    void set_value_from_row(const DBRow& row, tag<3>)
    {
        id = row["id"].get<decltype(id)>();
    }
    int id; 

    void set_value_from_row(const DBRow& row, tag<9>)
    {
        name = row["name"].get<decltype(name)>();
    }    
    std::string name;

    void set_value_from_row(const DBRow& row, tag<15>)
    {
        age = row["age"].get<decltype(age)>();
    }      
    int age;    
};

これで整数情報を付与し、シーケンスにできました。
tagの数字を0から増やしながら
set_value_from_rowを呼びまくるイメージです。

今回の例だとマクロの実装はこれでほぼ完了になります。

シーケンスをぶん回す

シーケンスの上限について

さっそくですが、0からどこまで回すのか決めないといけないので、最大数があります。
このテクニックのデメリットの1つですが、この制約によりコードがでかすぎると動作しなくなります。

また、そもそもコンパイル時計算の性能的に、数字がでかすぎてもエラーになるのでいったん500くらいにしておきます。

inline constexpr size_t SET_VALUE_FROM_ROW_MAX = 500;

あわせて、マクロ側でもうっかり上限をこえたらコンパイル時にエラーで気づくようにstatic_assertを挟みます。

#define DB_COLUMN_IMPL(key, line)\
void set_value_from_row(const DBRow& row, tag<line>)\
{\
   static_assert(line < SET_VALUE_FROM_ROW_MAX);\
   key = row[#key].get<decltype(key)>();\
}
#define DB_COLUMN(key) DB_COLUMN_IMPL(key, __LINE__)

呼び出す関数を絞り込む

まずコンセプトを作って、指定のタグを使うメソッドが定義されてるかどうか判定できるようにしておきます。

template<class Type, size_t Line>
concept settable_value_from_row = requires(Type value, const DBRow& row, tag<Line> tag)
{
    value.set_value_from_row(row, tag);
};

index_sequenceを作成

先ほど作成したコンセプトでフィルターして、必要なタグのみのindex_sequenceを作成します。

// index_sequenceのマージ
template <size_t... As, size_t... Bs>
constexpr std::index_sequence<As..., Bs...> operator+(std::index_sequence<As...>, std::index_sequence<Bs...>)
{
    return {};
}

// index_sequenceのフィルター
template <class Type, size_t Line>
constexpr auto filter_seq()
{
    if constexpr (settable_value_from_row<Type, Line>) {
        // このタグのセッターが存在するので呼ぶ必要がある
        return std::index_sequence<Line>{};
    } else {
        return std::index_sequence<>{};
    }
}

// index_sequenceの作成
template <class Type, size_t ...Seq>
constexpr auto make_sequence_impl(std::index_sequence<Seq...>)
{
    return (filter_seq<Type, Seq>() + ...);
}
template <class Type>
constexpr auto make_sequence()
{
    // 最大値までのシーケンスのうち必要なものだけにフィルター
    return make_sequence_impl<Type>(std::make_index_sequence<SET_VALUE_FROM_ROW_MAX>());
}

ぶん回す

上記で必要なタグに絞り込んだindex_sequenceを作成できたので、あとは呼ぶだけです。

// シーケンスが一つでもあるか
template<class Type>
concept auto_settable_from_row = decltype(make_sequence<Type>())::size() > 0;

// セッターを1つ呼ぶ
template<auto_settable_from_row Type, size_t Line>
void auto_set_from_row(Type& ret, const DBRow& row)
{
    ret.set_value_from_row(row, tag<Line>{});
}

// セッターを全部呼ぶ
template<auto_settable_from_row Type, size_t ...Seq>
void auto_set_from_row_all_impl(Type& ret, const DBRow& row, std::index_sequence<Seq...>)
{
    (auto_set_from_row<Type, Seq>(ret, row), ...);
}
template<auto_settable_from_row Type>
void auto_set_from_row_all(Type& ret, const DBRow& row)
{
    auto_set_from_row_all_impl(ret, row, make_sequence<Type>());
}

呼び出す窓口の整理

// DBRowからTに変換
template<class T>
T FromDBRow(const DBRow& row)
{
    T ret{};
    // セッターを全て呼ぶ
    auto_set_from_row_all(ret, row);
    return ret;
}

---------------------
    // DBRowからUser構築
    User user = FromDBRow<User>(row);

量産

無事にやりたいことができました。
あとは、他の構造体が増えても同様にメンバ変数とセットでマクロを1行たすだけで、
自動で構造体への変換ができるわけですから、量産も楽です。

struct Item
{
    [[DB_COLUMN(user_id)]]
    int user_id; 

    [[DB_COLUMN(item_id)]]
    int item_id; 

    [[DB_COLUMN(count)]]
    int count;    
};

struct Stage
{
    [[DB_COLUMN(user_id)]]
    int user_id; 

    [[DB_COLUMN(stage_id)]]
    int stage_id; 

    [[DB_COLUMN(is_clear)]]
    bool is_clear;    
};

---------------------
    // DBRowからItem構築
    Item item = FromDBRow<Item>(row);
---------------------
    // DBRowからStage構築
    Stage stage = FromDBRow<Stage>(row);

コード全体

コード全体をwandboxに置いておきました。
(ただしDBRowは妄想だったので仮実装に差し替えてあります)
https://wandbox.org/permlink/fuFqCrpNrdSTp8Lk

実用例

今回紹介したテクニックを使用した簡単なライブラリを何個か作ってるので
実用例として紹介をします。


DIでのフィールドインジェクション

class IPrinter
{
public:
    virtual void println(std::string_view str) const = 0;
};

class CoutPrinter : public IPrinter
{
public:
    void println(std::string_view str) const override
    {
        std::cout << str << std::endl;
    }
};

struct CoutInstaller : IInstaller
{
    void onBinding(Container* c) const
    {
        c->bind<IPrinter>()
            .to<CoutPrinter>()
            .asCache();
    }
};

class HelloWorld
{
public:
    HelloWorld() = default;

    void greet() const
    {
        m_printer->println("Hello World");
    }
private:
    [[INJECT(m_printer)]]
    std::shared_ptr<IPrinter> m_printer;
};

int main()
{
    Injector injector;
    injector.install<CoutInstaller>();

    auto helloWorld = injector.resolve<HelloWorld>();
    helloWorld->greet();
}

簡単なリフレクション

struct Test
{
    [[REFLECTION(func)]]
    void func() { std::cout << "Hello, Mkanta! Func" << std::endl; }
};

int main()
{
    using mkanta::reflect;
    // reflection member func
    if (auto func = reflect<Test>::find<void(Test::*)()>("func")) {
        Test a;
        (a.*func)();
    }
}

まとめ

  • C++ユーザー定義属性は作れない
  • 空の属性は無視されるので、マクロの前後につけとけば、なんちゃって属性「風」にできる
    • ただし、それだけなので本当に意味はない
  • マクロに行数(__LINE__)をタグとして仕込む事で、std::index_sequenceでぶん回せる
    • シーケンスの上限は決める必要があるのでコードが大きくなりすぎると動かないのはデメリット
32
14
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
14