以前作った実行時ジェネリクスのためのAnyは自由度を高め可変長引数や完全転送に対応させるために非常に入り組んだ設計になってしまったので、ずっと簡単でC++初級者にも分かりやすい設計にしてみようという試み。拘り過ぎると自分にとっては便利でも他人には役に立たないものになると身を持って知ったので、出来る限り分かりやすくしてみたつもりだ。
例えば、std::anyに次のような条件を設けられないだろうか。
- 足し算可能なオブジェクトのみ格納を許す。
- std::any_castせずに(つまり格納されている型を知らずとも)足し算を行える。
つまり、足し算さえできればあらゆるオブジェクトを許すstd::anyだ。もちろん足し算でなければならない理由は一切ないので、メンバ関数呼び出しをしたいとか、std::coutへ出力したいとかの要求があるのなら適宜読み替えてほしい。
もしstd::variantであればstd::visitを使うことで格納されたオブジェクトについて知らずとも処理できるのだが、std::anyはそうはいかない。std::anyの中身を扱うには必ずstd::any_castを使う必要があって、そのためには何が格納されているのかを何らかの方法で知る必要がある。
残念ながら、std::visitのような超便利な仕組みを作ることは不可能だろう。しかしstd::anyを継承し、予め行いたい処理を知っている派生クラスを作ることで、限定的ながら実現することはできる。std::anyの仕組みを応用すればいいのだ。実行時型情報を知っており、適切にstd::anyをキャストしつつ関数呼び出しを行える関数ポインタを取得しておくのである。
#include <any> #include <iostream> #include <vector> #include <string> class AnyAddable : public std::any { template <class T> static AnyAddable Add_impl(const AnyAddable& a, const AnyAddable& b) { auto& aa = std::any_cast<const T&>(a); auto& bb = std::any_cast<const T&>(b); return aa + bb; } public: template <class T> AnyAddable(T&& t) : std::any(std::forward<T>(t)), mFunc(&Add_impl<std::decay_t<T>>) {} template <class T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, AnyAddable>, std::nullptr_t> = nullptr> AnyAddable& operator=(T&& t) { static_cast<std::any&>(*this) = std::forward<T>(t); mFunc = &Add_impl<std::decay<T>>; return *this; } AnyAddable operator+(const AnyAddable& a) const { return mFunc(*this, a); } private: using FuncType = AnyAddable(const AnyAddable&, const AnyAddable&); FuncType* mFunc;//ここに足し算を行うための関数ポインタを保持しておく。関数ポインタなので格納されている型情報は必要ない。 }; AnyAddable AddAny(const AnyAddable& a, const AnyAddable& b) { //この関数はAnyAddableの中にどんな方のオブジェクトが格納されているのかを一切知らない。 //しかしa + bはAnyAddable内の関数ポインタを経由して適切に呼び出される。 return a + b; } int main() { //intを格納してみる。 AnyAddable int_a = 1, int_b = 2; auto int_c = AddAny(int_a, int_b); std::cout << std::any_cast<int>(int_c) << std::endl; //std::stringを格納してみる。 AnyAddable str_a = std::string("abc"), str_b = std::string("def"); auto str_c = AddAny(str_a, str_b); std::cout << std::any_cast<const std::string&>(str_c) << std::endl; try { //int + std::stringはstd::any_castに失敗し例外が飛んでくる。 auto no_result = AddAny(int_a, str_a); } catch (std::bad_any_cast e) { std::cout << e.what() << std::endl; } //std::vectorは足し算できないのでコンパイルエラー。 /* AnyAddable vec_a = std::vector<int>{ 1, 2, 3 }, vec_b = std::vector<int>{ 4, 5, 6 }; auto vec_c = AddAny(vec_a, vec_b); */ }
AnyAddableはstd::anyの派生クラスで、テンプレート引数を持たず、静的な型情報が削除されている。したがって、std::anyと同様に型情報を得ることができない状況であっても扱うことが出来る。
しかしAnyAddableはその内部に足し算を行うための関数が定義されている。これはメンバ変数mFuncに保存された関数Add_impl<T>へのポインタを経由して、AnyAddableそのままで足し算を行える。
AddAnyという関数に注目してほしい。通常、汎用的な型に対応する関数を定義するときはテンプレートを使うものだが、AddAnyはテンプレートではないにも関わらず、つまり格納されている型の情報を一切知らないにも関わらず、int型、std::string型両者の足し算を適切に行なっている。Add_implが持つ型情報を利用することでこのような処理も実現できるのだ。
この仕組みを徹底的に汎化していくとえげつないことになるのだが、即席で制限付きstd::anyを作りたい場合はこのくらい簡略化することが出来るという例。 ……しかし最近std::anyやその類似品で遊んでばかりいるなぁ。Type Erasureが楽しすぎるのがいけないんだ。