ことの発端
先日、こんな記事を投稿したのですが
その振り返りで
ラムダ式にrefcon無くしたいかも
というものを実現しようかという話です。
やりたいこと
- 可変引数テンプレートの
N
番目のパラメータを除いたstd::tuple
を作りたい。 -
N
番目は非型テンプレートパラメータとしたい。 -
std::tie
とstd::ignore
で「値」を除去できるが、欲しいのは除去後の「型」なので今回はスコープとしよう。(declval
でゴニョゴニョできるかも??)
結論、こんな感じにしたい。
using t = omit<1, void*, int>;
/* t = std::tuple<int> */
やってみた
1. まず最初に
いきなりN
番目とか作れんので、まず最初の1個を除外するコードを書いてみる。
#include <tuple>
template <typename T1, typename ...ARGS> struct omit_1 {
using type = std::tuple<ARGS...>;
};
int main(void){
using t = omit_1<void*, int, int, float>::type;
/* t = std::tuple<int, int, float> */
}
ふむ、良い感じ。
2. N
を0~3とするとどんな感じになるか試してみる
やはり、ここでもN
番目とか作るんじゃなくて、N
が変化するとどんな感じのコードになるかをやってみるのが良いと思うわけで。。。
#include <tuple>
template <typename ...ARGS> struct omit_0 {
using type = std::tuple<ARGS...>;
};
template <typename T1, typename ...ARGS> struct omit_1 {
using type = std::tuple<ARGS...>;
};
template <typename T1, typename T2, typename ...ARGS> struct omit_2 {
using type = std::tuple<T1, ARGS...>;
};
template <typename T1, typename T2, typename T3, typename ...ARGS> struct omit_3 {
using type = std::tuple<T1, T2, ARGS...>;
};
int main(void){
using t0 = omit_0<void*, int, int, float>::type;
/* t0 = std::tuple<void *, int, int, float> */
using t1 = omit_1<void*, int, int, float>::type;
/* t1 = std::tuple<int, int, float> */
using t2 = omit_2<void*, int, int, float>::type;
/* t2 = std::tuple<void *, int, float> */
using t3 = omit_3<void*, int, int, float>::type;
/* t3 = std::tuple<void *, int, float> */
}
ちゃんと、動作確認もしつつ。。。
で、こうすると徐々にパターンが見えてくるので。。。
3. 特殊化使ってN
番目を表現してみる
私のような凡人には、命題を一般化することが難しいので、せめてN
を非型パラメータにできないかってことで、omit
という1次テンプレートを宣言して、omit
の特殊化を定義していく方針にする。
当然、N
個定義しなきゃならんけど、関数の引数で10個超えるなんてなかなか拝めるものじゃないので、10個くらい用意しとけば良いんじゃないかしら?(そこまで引数が増えたら構造体を渡すのが一般的じゃないかなぁ)
という、ことでまずは 0 ~ 3 を特殊化定義していみる。
template <int N, typename ...ARGS> struct omit;
template <int N, typename ...ARGS> struct omit<0, ARGS...> : public omit_0<ARGS...> {};
template <int N, typename ...ARGS> struct omit<1, ARGS...> : public omit_1<ARGS...> {};
template <int N, typename ...ARGS> struct omit<2, ARGS...> : public omit_2<ARGS...> {};
template <int N, typename ...ARGS> struct omit<3, ARGS...> : public omit_3<ARGS...> {};
4. エラーを潰す
すると。。。
error: template parameters not deducible in partial specialization:
[build] template <int N, typename ...ARGS> struct omit<0, ARGS...> : public omit_0<ARGS...> {};
[build] ^~~~~~~~~~~~~~~~
ふむ。。。
英語力が低いのでgoogle先生に翻訳してもらうと
部分的な特殊化で推論できないテンプレートパラメータ
メタプログラミングに慣れていないと、なかなかミスを見つけられない。
悩むこと数分、「はっ」と気付く。
template <int N, typename ...ARGS> struct omit;
template <typename ...ARGS> struct omit<0, ARGS...> : public omit_0<ARGS...> {};
template <typename ...ARGS> struct omit<1, ARGS...> : public omit_1<ARGS...> {};
template <typename ...ARGS> struct omit<2, ARGS...> : public omit_2<ARGS...> {};
template <typename ...ARGS> struct omit<3, ARGS...> : public omit_3<ARGS...> {};
そりゃそうだと。。。
5. 最後から N
番目もやってみる
これで、非型パラメータを使えるようになったので、最後から N
番目というのが書けるようになる。
template <int N, typename ...ARGS> struct romit {
using type = typename omit<std::tuple_size<std::tuple<ARGS...>>::value - N, ARGS...>::type;
};
6. できたものをテストしてみる
テストコードと言っても、型を取得するだけなので、今回は開発環境(VSCode)で定義にカーソル当てて型を確認することにした。
#include <tuple>
template <typename ...ARGS> struct omit_0 {
using type = std::tuple<ARGS...>;
};
template <typename T1, typename ...ARGS> struct omit_1 {
using type = std::tuple<ARGS...>;
};
template <typename T1, typename T2, typename ...ARGS> struct omit_2 {
using type = std::tuple<T1, ARGS...>;
};
template <typename T1, typename T2, typename T3, typename ...ARGS> struct omit_3 {
using type = std::tuple<T1, T2, ARGS...>;
};
template <int N, typename ...ARGS> struct omit;
template <typename ...ARGS> struct omit<0, ARGS...> : public omit_0<ARGS...> {};
template <typename ...ARGS> struct omit<1, ARGS...> : public omit_1<ARGS...> {};
template <typename ...ARGS> struct omit<2, ARGS...> : public omit_2<ARGS...> {};
template <typename ...ARGS> struct omit<3, ARGS...> : public omit_3<ARGS...> {};
template <int N, typename ...ARGS> struct romit {
using type = typename omit<std::tuple_size<std::tuple<ARGS...>>::value - N, ARGS...>::type;
};
int main(void){
using t0 = omit<0, void*, int, int*, float>::type;
/* t0 = std::tuple<void*, int, int*, float> */
using t1 = omit<1, void*, int, int*, float>::type;
/* t1 = std::tuple<int, int*, float> */
using t2 = omit<2, void*, int, int*, float>::type;
/* t2 = std::tuple<void*, int*, float> */
using t3 = omit<3, void*, int, int*, float>::type;
/* t3 = std::tuple<void*, int, float> */
// using t0r = romit<0, void*, int, int*, float>::type;
/* omit<4, ...> は特殊化されていないのでomitは不完全型(incomplete type)というエラー */
using t1r = romit<1, void*, int, int*, float>::type;
/* t1 = std::tuple<void*, int, float> */
using t2r = romit<2, void*, int, int*, float>::type;
/* t2 = std::tuple<void*, int*, float> */
using t3r = romit<3, void*, int, int*, float>::type;
/* t3 = std::tuple<int, int*, float> */
}
7.N
が 0 ~ 10 まで作りましょう
大丈夫そうなので、これを10個作りますか。。。
コピペで大量に作っても良いけど、効率を考えてマクロを作ることにしました。
(諸事情がありマクロを書くことが罪悪感しかないのだけど、堪えることにした)
0 と 1 だけは特殊なので、そのまま定義しちゃって、2 以降をマクロで作る感じにしました。
#include <tuple>
template <int N, typename ...ARGS> struct omit;
template <typename ...ARGS> struct omit_0 {
using type = std::tuple<ARGS...>;
};
template <typename ...ARGS> struct omit<0, ARGS...> : public omit_0<ARGS...> {};
template <typename T1, typename ...ARGS> struct omit_1 {
using type = std::tuple<ARGS...>;
};
template <typename ...ARGS> struct omit<1, ARGS...> : public omit_1<ARGS...> {};
#define __(V, TN, N) template <TN, typename ...ARGS> struct omit_##V { \
using type = std::tuple<N, ARGS...>; \
}; \
template <typename ...ARGS> struct omit<V, ARGS...> : public omit_##V<ARGS...> {};
#define _(...) __VA_ARGS__
__( 2, _(typename T1, typename T2), _(T1))
__( 3, _(typename T1, typename T2, typename T3), _(T1, T2))
__( 4, _(typename T1, typename T2, typename T3, typename T4), _(T1, T2, T3))
__( 5, _(typename T1, typename T2, typename T3, typename T4, typename T5), _(T1, T2, T3, T4))
__( 6, _(typename T1, typename T2, typename T3, typename T4, typename T5, typename T6), _(T1, T2, T3, T4, T5))
__( 7, _(typename T1, typename T2, typename T3, typename T4, typename T5, typename T6, typename T7), _(T1, T2, T3, T4, T5, T6))
__( 8, _(typename T1, typename T2, typename T3, typename T4, typename T5, typename T6, typename T7, typename T8), _(T1, T2, T3, T4, T5, T6, T7))
__( 9, _(typename T1, typename T2, typename T3, typename T4, typename T5, typename T6, typename T7, typename T8, typename T9), _(T1, T2, T3, T4, T5, T6, T7, T8))
__(10, _(typename T1, typename T2, typename T3, typename T4, typename T5, typename T6, typename T7, typename T8, typename T9, typename T10), _(T1, T2, T3, T4, T5, T6, T7, T8, T9))
#undef _
#undef __
template <int N, typename ...ARGS> struct romit {
using type = typename omit<std::tuple_size<std::tuple<ARGS...>>::value - N, ARGS...>::type;
};
所感
多分、こんなのSTLやboostを探せば出てくるのかもね~
まぁ、この手の簡単な機能だったら、(時間があれば)解決方法を模索する(ライブラリを探すじゃなく)のもプログラムの醍醐味だったりする。
とはいえ、品質で考えたらSTLやboostの方が断然上(テスト量が桁違うし)なので、そのうち今回と同機能を見つけたら、このコードは破棄するつもり。更にドキュメントとか仕様とかしっかりしているので、チーム開発における「情報伝搬」・「認識合わせ」の手間が省けて効率的。
ただ、自分であれやこれやと実装方法を模索した後に、ライブラリに出会えれば、答え合わせが楽しいし。
(ある意味、贅沢な方法かもですね)
さて。。。 こいつと同機能なライブラリを探してみようかしら。。。
神が降臨しました(2021/03/08 追記)
哀れな迷える子羊に神が降臨しました。
@myoukaku さんありがとうございます!
@myoukaku さんのコードには本質的な手法以外にも情報が詰まっておりまして、神様のお告げにはこう書かれていました。
- template における再帰の手法はこうだよ
-
int
じゃなくstd::size_t
使おうね -
static_assert
でテストコード書けるよ -
struct
はアクセス指定子を指定しなければpublic
継承になるよ
神様ごめんなさい。。。
いや、ありがとうございます。
神様のコードを理解するまで眺めて。。。
コードを閉じる → 頭でイメージ → シャワーを浴びる → ごはんを食べる
そして、書いてみました!1
#include <tuple>
template<std::size_t, std::size_t, typename ...> struct omit_impl;
template <std::size_t N, std::size_t I, typename ...TPARGS, typename T, typename ...ARGS>
struct omit_impl<N, I, std::tuple<TPARGS...>, T, ARGS...> :
omit_impl<N, I + 1, std::tuple<TPARGS..., T>, ARGS...> {};
template <std::size_t N, typename ...TPARGS, typename T, typename ...ARGS>
struct omit_impl<N, N, std::tuple<TPARGS...>, T, ARGS...> :
omit_impl<N, N + 1, std::tuple<TPARGS... /*, T */>, ARGS...> {};
template <std::size_t N, std::size_t I,typename ...TPARGS>
struct omit_impl<N, I, std::tuple<TPARGS...>>
{
using type = std::tuple<TPARGS...>;
};
template <std::size_t N, typename ...ARGS>
struct omit :
omit_impl<N, 0, std::tuple<>, ARGS...> {};
int main(void){
using T0 = int;
using T1 = float;
using T2 = double;
using T3 = int*;
using T4 = float*;
static_assert(std::is_same<omit<0>::type, std::tuple<>>::value);
static_assert(std::is_same<omit<0, T0>::type, std::tuple<>>::value);
static_assert(std::is_same<omit<0, T0, T1>::type, std::tuple<T1>>::value);
static_assert(std::is_same<omit<1, T0, T1>::type, std::tuple<T0>>::value);
static_assert(std::is_same<omit<0, T0, T1, T2>::type, std::tuple<T1, T2>>::value);
static_assert(std::is_same<omit<1, T0, T1, T2>::type, std::tuple<T0, T2>>::value);
static_assert(std::is_same<omit<2, T0, T1, T2>::type, std::tuple<T0, T1>>::value);
static_assert(std::is_same<omit<0, T0, T1, T2, T3>::type, std::tuple<T1, T2, T3>>::value);
static_assert(std::is_same<omit<1, T0, T1, T2, T3>::type, std::tuple<T0, T2, T3>>::value);
static_assert(std::is_same<omit<2, T0, T1, T2, T3>::type, std::tuple<T0, T1, T3>>::value);
static_assert(std::is_same<omit<3, T0, T1, T2, T3>::type, std::tuple<T0, T1, T2>>::value);
static_assert(std::is_same<omit<0, T0, T1, T2, T3, T4>::type, std::tuple<T1, T2, T3, T4>>::value);
static_assert(std::is_same<omit<1, T0, T1, T2, T3, T4>::type, std::tuple<T0, T2, T3, T4>>::value);
static_assert(std::is_same<omit<2, T0, T1, T2, T3, T4>::type, std::tuple<T0, T1, T3, T4>>::value);
static_assert(std::is_same<omit<3, T0, T1, T2, T3, T4>::type, std::tuple<T0, T1, T2, T4>>::value);
static_assert(std::is_same<omit<4, T0, T1, T2, T3, T4>::type, std::tuple<T0, T1, T2, T3>>::value);
}
今回の神様のお言葉で特に感銘を受けたのは特殊化のパラメータで std::tuple<TPARGS...>
で TPARGS
をキャプチャする箇所と、omit_impl<N, N,
で 値が同一という表現をする箇所が、グッときました。
言われれば、そりゃそうなんだけど、何と言うか。。。特殊化の面白しろさというか奥の深さを知った感じです。
@myoukaku さんのお陰で楽しい週末になりました。
ありがとうございます!
新しい技を覚えたので(2021/03/09 追記)
その後、@myoukaku さんから std::conditional
という新しい技を伝授いただいたので、リファクタリングしました。
template<std::size_t, std::size_t, typename ...> struct omitargs_impl;
template <std::size_t N, std::size_t I, typename ...TPARGS, typename T, typename ...ARGS>
struct omitargs_impl<N, I, std::tuple<TPARGS...>, T, ARGS...> :
std::conditional<
I == N,
omitargs_impl<N, I + 1, std::tuple<TPARGS...>, ARGS...>,
omitargs_impl<N, I + 1, std::tuple<TPARGS..., T>, ARGS...>
>::type {};
template <std::size_t N, std::size_t I,typename ...TPARGS>
struct omitargs_impl<N, I, std::tuple<TPARGS...>>
{
using type = std::tuple<TPARGS...>;
};
template <std::size_t N, typename ...ARGS> struct omitargs {
static_assert(N >= 0, "N < 0");
static_assert(N < std::tuple_size<std::tuple<ARGS...>>::value, "N >= ARGS");
using type = typename omitargs_impl<N, 0, std::tuple<>, ARGS...>::type;
};
template <std::size_t N, typename ...ARGS> struct omitargs_r {
static_assert(N >= 0, "N < 0");
static_assert(N < std::tuple_size<std::tuple<ARGS...>>::value, "N >= ARGS");
using type = typename omitargs<std::tuple_size<std::tuple<ARGS...>>::value - N - 1, ARGS...>::type;
};
この前のコードでは<N, N
のあたりが「技使ってるで~カッコエエ~」的な感じ(主観です)でしたが、こちらは地味ながらも(主観です)条件分岐が明確なので、後から読んで理解しやすいなぁ~と思います。
-
実は当初は
N
番目が1
始まりでしたが、神様の仕様(0
始まり)の方が直感的なので仕様変更しました。 ↩