🐙

型情報を渡したいときの小技

2024/12/07に公開
2

この記事はC++ Advent Calendar 2024の6日目の記事です。(埋まってなかったので急遽書いた)

TL;DR

std::type_identity<T>は状態を持たない(monostateな)ことを利用すると型情報だけを渡せる。

追記: std::in_place_type_tを使う方が良さそう。

本文

hoge.h
#ifdef __cplusplus
extern "C" {
#endif
enum Type {
    T1,
    T2,
    /* 略 */
};
typedef enum Type type_t;
int func(void* p, type_t type, /* たくさんの引数 */);
#ifdef __cplusplus
}
#endif
hoge.cpp
#include "hoge.h"

int func(void* p, type_t type, /* たくさんの引数 */) {
    switch(type) {
        case T1:
            // T1のときの処理
            return 0;
        case T2:
            // T2のときの処理
            return 0;
        /* 略 */
        default:
            return -1;
    }
}

こんなコードがあったとします。hoge.cppはC++20を使えることとして、各型での処理は似ているとします。
まずはテンプレートで処理したくなりますね。

hoge.cpp
template<typename T>
int aux(T* p, /*たくさんの引数*/) {
    // 処理
    return 0;
}

int func(void* p, type_t type, /* たくさんの引数 */) {
    switch(type) {
        case T1:
            return aux(reinterpret_cast<T1*>(p), /* たくさんの引数 */);
        case T2:
            return aux(reinterpret_cast<T2*>(p), /* たくさんの引数 */);
        /* 略 */
        default:
            return -1;
    }
}

たくさんの引数を毎回書くのは面倒ですね。マクロで解決する手もありますがそれはせずに、ラムダ式でキャプチャすることにします。

hoge.cpp
int func(void* p, type_t type, /* たくさんの引数 */) {
    auto f = [&](auto dummy) {
        // 処理
        return 0;
    };
    switch(type) {
        case T1:
            return f(T1{});
        case T2:
            return f(T2{});
        /* 略 */
        default:
            return -1;
    }
}

大分スッキリしてきましたね。ただ、ダミー引数が気になります。上のコードではdummyという変数名にしているので使わないことが明らかかもしれませんが、処理の部分でdummyの値に依存した処理を書けてしまいます。また、T1T2の値を構築するコストが大きいことも考えられます。
C++20だとジェネリックラムダのテンプレート構文が使えるので引数の代わりにこれを使って型情報を渡すことが出来ます。

hoge.cpp
int func(void* p, type_t type, /* たくさんの引数 */) {
    auto f = [&]<typename T>() {
        // 処理
        return 0;
    };
    switch(type) {
        case T1:
            return f.template operator()<T1>();
        case T2:
            return f.template operator()<T2>();
        /* 略 */
        default:
            return -1;
    }
}

これで上で述べたダミー引数の問題は解消できます。このままでも良いですが、std::type_identity<T>が状態を持たないことを用いて私は以下のようにしました。
追記: コメントで教えて頂いたstd::in_place_type_tを使う方が良さそう。

hoge.cpp
#include <type_traits>

int func(void* p, type_t type, /* たくさんの引数 */) {
    auto f = [&]<typename T>(std::type_identity<T>) {
        // 処理
        return 0;
    };
    switch(type) {
        case T1:
            return f(std::type_identity<T1>{});
        case T2:
            return f(std::type_identity<T2>{});
        /* 略 */
        default:
            return -1;
    }
}

おわりに

型推論の無効化のためのstd::type_identity<T>を悪用(?)するとこんなことも出来るのはちょっと面白いですね。
ちなみにこの方法を思いついたのは以下のツイートを見たからというのもあります。
https://x.com/dif_engine/status/1841028116854292491

Discussion

齊藤敦志齊藤敦志

そのような用途のために std::in_place_type_t があります。