任意の型の参照を受け取ることのできる型が欲しい、と思ったことはないだろうか。例えば関数テンプレートに出来ない仮想関数で、引数に何が与えられるか分からないとき、どんな型であろうととりあえず参照で受け取ることができればと思ったことはないだろうか。
ついでに、そのようなよく分からない引数に対して何らかの処理を行いたい、なんてこともありうる1。例えば、std::coutに出力する、std::stringに変換する、あるメンバ関数funcを呼び出す、などだ。関数テンプレートを用いない汎用的な参照でありながらこんな処理をする方法はあるのだろうか?
struct A { //refには任意の値の参照を与えたい。 //場合によってはrefに対して何らかの処理をしたい。ref.func(arg1, arg2)みたいに。 //しかし仮想関数なのでテンプレートは使えない。無論Aもクラステンプレートに出来ない。 virtual int func(any_reference ref) { ... } };
単なる汎用参照であればいくつかの方法が考えられるが、標準ライブラリの範疇では今ひとつスマートに実現できない。ジェネリクス的な処理の呼び出しに関しては単純に実現する方法はなさそうである。これらは結局のところ、Type Erasureによるジェネリクスは可能なのか、という話に集約される。4.のソースコードを参照してほしい。
1. 汎用ポインタ
最も単純で愚かしい方法は、汎用ポインタ(void*)だ。C言語時代から存在する由緒正しき汚物である。
安易に使いたくなる単純さであるが、型情報を喪失するのは致命的である。代わりにenumとかに型情報を残しておけとでも言うのだろうか?まさかtype_infoを使わせる気か?そんな方法が許されるのは初心者までだ(つい最近まで私も使っていた。つまり私はほぼ初心者……)。
また、個人的にポインタで渡すのは好かない。いちいち&記号を入力するのが手間である。
2. std::any
std::anyを使う、というのは誰もが想像するかもしれないが、今回の場合は不適切だ。なぜなら今回引数に与えたいのは参照型であり、std::anyは参照型を格納できないからだ。参照型を頑張ってラップしたりすれば実現できないことはないだろうが、手間がかかるだけだ。まあでもポインタで渡すという方法は取れる(ただし実装によっては動的メモリ確保のコストが発生する)ため、一応目的は達成できる。
あるいはもし値のコピーやムーブ、動的メモリ確保が発生しても良いのなら有力な選択肢として考えて良いだろう。
尤も、ジェネリクス的に処理することは出来ないので、こちらも最終的には型を把握できなければならない。
3. std::variant
std::variantもstd::anyと同様に参照型を格納できないので、賢明な方法ではない。しかしstd::variantは事前に全ての型を与えておく仕様になっているので、これを逆手に取って全ての型をstd::reference_wrapperで包むことで、望む動作はある程度実現するだろう。
template <class ...T> using variant_ref = std::variant<std::reference_wrapper<T>...>; using argtype = variant_ref<const std::string, const double, const std::vector<double>>; void func2(argtype a) { if (a.index() == 0) std::cout << "a is string " << std::get<0>(a).get() << std::endl; else if (a.index() == 1) std::cout << "a is double " << std::get<1>(a).get() << std::endl; else if (a.index() == 2) { std::cout << "a is vector<double>"; for (auto v : std::get<2>(a).get()) std::cout << " " << v; std::cout << std::endl; } }
ただしstd::variantは事前に与えられる可能性のあるすべての型を把握していなければならないため、std::any以上に自由度に乏しい。代わりにstd::visitという強力な関数があるので、簡単にジェネリクスもどきを実現したいのなら有意義である。
4. 参照型のanyを作る。
私が今回採用したのは、型情報を保持できる汎用参照型を作るという方法だ。std::anyが参照型に対応しないのなら、対応する参照版のanyを用意すればいいじゃない、という単純な回答である。
当初は以前記事にしたStaticAnyを使い動作するところまで作ったのだが、あちらはDLL boundariesに関する問題が発覚したので、代わりに仮想関数を用いた方法を改めて採用し作り直した。Type Erasureでよく使われるテクニックの流用なので、別に新しいことは何もしていない。
とりあえず、MSVC2019、GCC10.1、Clang10.0で動作することを確認した。本コードは過度な複雑化を避けるため一部機能を省略している(そして作り込みが半端なためオーバーヘッドが大きい)。GitHubではもう少しきちんと実装したものを公開している。
#include <cassert> #include <type_traits> #include <utility> #include <tuple> #include <typeindex> namespace anyref { namespace detail { template <class T> using RemoveCVRefT = std::remove_cv_t<std::remove_reference_t<T>>; template <template <class> class Modifier> class AnyRef_impl { class HolderBase { public: virtual ~HolderBase() = default; virtual void CopyTo(void* b) const = 0; virtual std::type_index GetTypeIndex() const = 0; }; template <class T> class Holder : public HolderBase { public: Holder() = default; Holder(Modifier<T> v) : mValue(static_cast<Modifier<T>>(v)) {} Holder(const Holder& h) : mValue(static_cast<Modifier<T>>(h.mValue)) {} virtual void CopyTo(void* ptr) const { Holder<T>* p = reinterpret_cast<Holder<T>*>(ptr); new (p) Holder<T>(*this); } virtual std::type_index GetTypeIndex() const { return typeid(T); } Modifier<T> mValue; }; template <class Type> void Construct(Type&& v) { using Type_ = RemoveCVRefT<Type>; static_assert(sizeof(Holder<Type_>) >= sizeof(mStorage), "the size of buffer is insufficient."); Holder<Type_>* p = reinterpret_cast<Holder<Type_>*>(&mStorage); new (p) Holder<Type_>(static_cast<Modifier<Type_>>(v)); } public: template <class Type, std::enable_if_t<!std::is_same_v<RemoveCVRefT<Type>, AnyRef_impl>, std::nullptr_t> = nullptr> AnyRef_impl(Type&& v) { Construct(std::forward<Type>(v)); } AnyRef_impl(const AnyRef_impl& a) { reinterpret_cast<const HolderBase*>(&a.mStorage)->CopyTo(&mStorage); } template <class Type, std::enable_if_t<!std::is_same_v<std::remove_cv_t<Type>, AnyRef_impl>, std::nullptr_t> = nullptr> AnyRef_impl& operator=(Type&& v) { HolderBase* p = reinterpret_cast<HolderBase*>(&mStorage); p->~HolderBase(); Construct(std::forward<Type>(v)); return *this; } AnyRef_impl& operator=(const AnyRef_impl& a) { //destroy myself HolderBase* p = reinterpret_cast<HolderBase*>(&mStorage); p->~HolderBase(); //copy const HolderBase* q = reinterpret_cast<const HolderBase*>(&a.mStorage); q->CopyTo(&mStorage); return *this; } template <class Type> Modifier<Type> Get() const { const Holder<Type>* p = GetHolder<Type>(); assert(p != nullptr); return static_cast<Modifier<Type>>(p->mValue); } template <class Type> bool Is() const { return GetHolder<Type>() != nullptr; } std::type_index GetTypeIndex() const { return GetHolderBase()->GetTypeIndex(); } private: template <class Type> const Holder<Type>* GetHolder() const { const HolderBase* p = std::launder(reinterpret_cast<const HolderBase*>(&mStorage)); return dynamic_cast<const Holder<Type>*>(p); } const HolderBase* GetHolderBase() const { return std::launder(reinterpret_cast<const HolderBase*>(&mStorage)); } std::aligned_storage_t<16> mStorage; }; template <class T> using NonConstRef = std::add_lvalue_reference_t<T>; template <class T> using ConstRef = std::add_lvalue_reference_t<std::add_const_t<T>>; template <class T> using RvalueRef = std::add_rvalue_reference_t<T>; } class AnyRef : public detail::AnyRef_impl<detail::NonConstRef> { public: template <class Type, std::enable_if_t<!std::is_same_v<std::remove_const_t<Type>, AnyRef> && !std::is_const_v<Type>, std::nullptr_t> = nullptr> AnyRef(Type& v) : detail::AnyRef_impl<detail::NonConstRef>(v) {} }; class AnyCRef : public detail::AnyRef_impl<detail::ConstRef> { public: template <class Type, std::enable_if_t<!std::is_same_v<Type, AnyCRef>, std::nullptr_t> = nullptr> AnyCRef(const Type& v) : detail::AnyRef_impl<detail::ConstRef>(v) {} }; class AnyRRef : public detail::AnyRef_impl<detail::RvalueRef> { public: template <class Type, std::enable_if_t<!std::is_same_v<Type, AnyRRef>&& std::is_rvalue_reference_v<Type&&>, std::nullptr_t> = nullptr> AnyRRef(Type&& v) : detail::AnyRef_impl<detail::RvalueRef>(std::move(v)) {} };
AnyRefを使うと、どんな型であってもとりあえず参照渡しできる。AnyRefは非const参照、AnyCRefがconst参照、AnyRRefが右辺値参照に対応する。
//任意の型を受け取ることができるが、int、double、std::string以外は全部その他扱い。 //そういう意味ではstd::variantやstd::anyと大差ない。 void FuncAnyCRef(AnyCRef a) { if (a.Is<int>()) std::cout << "a is int " << a.Get<int>() << std::endl; else if (a.Is<double>()) std::cout << "a is double " << a.Get<double>() << std::endl; else if (a.Is<std::string>()) std::cout << "a is std::string " << a.Get<std::string>() << std::endl; else std::cout << "a is " << a.GetTypeIndex().name() << std::endl; } int main() { FuncAnyCRef(1); FuncAnyCRef(2.); FuncAnyCRef(std::string("3")); FuncAnyCRef(4.f);//floatは対応していないので、型名のみ出力される。 return 0; }
GitHubの方ではAnyURefというユニバーサル参照相当のものも用意されている。
またGenericsというクラスを用意し、AnyRefに格納されている型に対して何らかの行いたい処理をVisitorクラスとして与えておき、関数テンプレートのように適切な型へとキャストしつつ処理を呼べるようにした。発想としてはType Erasureのそれと同じで、コンストラクタで引数の型情報を保持する派生クラスを生成しておき、関数Visitで呼び出しているだけである。
template <class Refs, class Visitors> class Generics; template <class ...Refs, class ...Visitors> class Generics<std::tuple<Refs...>, std::tuple<Visitors...>> { template <class Indices, class Visitors_> class VisitBase_impl; template <class Visitor, size_t Index> class VisitBase_impl<std::tuple<Visitor>, std::index_sequence<Index>> { public: using ArgTypes = typename Visitor::ArgTypes; using RetType = typename Visitor::RetType; virtual RetType Visit(std::integral_constant<size_t, Index>, const std::tuple<Refs...>& refs, ArgTypes args) const = 0; }; template <class Visitor, class ...Visitors_, size_t Index, size_t ...Indices> class VisitBase_impl<std::tuple<Visitor, Visitors_...>, std::index_sequence<Index, Indices...>> : public VisitBase_impl<std::tuple<Visitors_...>, std::index_sequence<Indices...>> { public: using Base = VisitBase_impl<std::tuple<Visitors_...>, std::index_sequence<Indices...>>; using Base::Visit; using ArgTypes = typename Visitor::ArgTypes; using RetType = typename Visitor::RetType; virtual RetType Visit(std::integral_constant<size_t, Index>, const std::tuple<Refs...>& refs, ArgTypes args) const = 0; }; class VisitBase : public VisitBase_impl<std::tuple<Visitors...>, std::make_index_sequence<sizeof...(Visitors)>> { public: virtual void CopyTo(void*) const = 0; }; template <class Types, class TIndices, class Visitors_, class VIndices> class VisitDerivative_impl; template <class ...Types, size_t ...TIndices, class Visitor, size_t VIndex> class VisitDerivative_impl<std::tuple<Types...>, std::index_sequence<TIndices...>, std::tuple<Visitor>, std::index_sequence<VIndex>> : public VisitBase { public: using VisitBase::Visit; using ArgTypes = typename Visitor::ArgTypes; using RetType = typename Visitor::RetType; virtual RetType Visit(std::integral_constant<size_t, VIndex>, const std::tuple<Refs...>& refs, ArgTypes args) const { auto tup = std::tuple_cat(std::forward_as_tuple(std::get<TIndices>(refs).template Get<Types>()...), args); return std::apply(Visitor(), tup); } }; template <class ...Types, size_t ...TIndices, class Visitor, class ...Visitors_, size_t VIndex, size_t ...VIndices> class VisitDerivative_impl<std::tuple<Types...>, std::index_sequence<TIndices...>, std::tuple<Visitor, Visitors_...>, std::index_sequence<VIndex, VIndices...>> : public VisitDerivative_impl<std::tuple<Types...>, std::index_sequence<TIndices...>, std::tuple<Visitors_...>, std::index_sequence<VIndices...>> { public: using Base = VisitDerivative_impl<std::tuple<Types...>, std::index_sequence<TIndices...>, std::tuple<Visitors_...>, std::index_sequence<VIndices...>>; using Base::Visit; using ArgTypes = typename Visitor::ArgTypes; using RetType = typename Visitor::RetType; virtual RetType Visit(std::integral_constant<size_t, VIndex>, const std::tuple<Refs...>& refs, ArgTypes args) const { return std::apply(Visitor(), std::tuple_cat(std::forward_as_tuple(std::get<TIndices>(refs).template Get<Types>()...), args)); } }; template <class ...Types> class VisitDerivative : public VisitDerivative_impl<std::tuple<Types...>, std::make_index_sequence<sizeof...(Types)>, std::tuple<Visitors...>, std::make_index_sequence<sizeof...(Visitors)>> { public: virtual void CopyTo(void* base) const { VisitDerivative* p = reinterpret_cast<VisitDerivative*>(base); new (p) VisitDerivative(*this); } }; public: //Refsが1個のみの場合、argsがtupleか否かに関わらず問答無用でstd::tupleにパックされたままmRefsの<0>番目に格納される。 //結果、mRefsの中身はTypesの<0>番目ではなく、std::tuple<Types...>となってしまう。 //これを回避するため、引数が1個のときだけは処理を分岐させる。 template <class ...Types, std::enable_if_t<sizeof...(Types) != 1, std::nullptr_t> = nullptr> Generics(std::tuple<Types...> args) : mRefs(args) { using Visit_ = VisitDerivative<detail::RemoveCVRefT<Types>...>; static_assert(sizeof(Visit_) >= sizeof(mVisitors), "the size of storage is insufficient."); Visit_* p = reinterpret_cast<Visit_*>(&mVisitors); new (p) Visit_(); } template <class Type> Generics(std::tuple<Type> args) : mRefs(std::get<0>(args)) { using Visit_ = VisitDerivative<detail::RemoveCVRefT<Type>>; static_assert(sizeof(Visit_) >= sizeof(mVisitors), "the size of storage is insufficient."); Visit_* p = reinterpret_cast<Visit_*>(&mVisitors); new (p) Visit_(); } Generics& operator=(const Generics& g) { VisitBase* p = reinterpret_cast<VisitBase*>(&mVisitors); p->~VisitBase(); reinterpret_cast<VisitBase*>(&g.mVisitors)->CopyTo(this); } template <size_t Index> decltype(auto) GetRef() const { return std::get<Index>(mRefs); } template <size_t Index, class Type> decltype(auto) Get() const { return std::get<Index>(mRefs).template Get<Type>(); } template <size_t Index, class ...Args> decltype(auto) Visit(Args&& ...args) const { using Visitor = std::tuple_element_t<Index, std::tuple<Visitors...>>; using ArgTypes = typename Visitor::ArgTypes; return reinterpret_cast<const VisitBase*>(&mVisitors)->Visit(std::integral_constant<size_t, Index>(), mRefs, ArgTypes(std::forward<Args>(args)...)); } private: std::tuple<Refs...> mRefs; std::aligned_storage<8> mVisitors; }; }
Genericsには事前にVisitorクラス(呼び出す処理を記述したクラス)をテンプレートパラメータに与えておく必要があるが、一応当初の目的であった仮想関数など関数テンプレートを用いることの出来ない状況下でのジェネリクス的処理は実現した。それこそstd::variantで良くない?と言われかねないが、条件さえ満たせばどんな型でもよいという自由度を持たせられるので、一応メリットには違いない。
example1 Genericsクラスの使い方
struct Visitor0_0 { using ArgTypes = std::tuple<>; using RetType = void; template <class T> void operator()(const T& v) { for (auto d : v) std::cout << d; std::cout << std::endl; } }; struct Visitor0_1 { using ArgTypes = std::tuple<>; using RetType = int;//戻り値はRetTypeに与える。 template <class T> int operator()(const T& v) { return std::accumulate(v.begin(), v.end(), 0); } }; struct Visitor0_2 { using ArgTypes = std::tuple<int, int>;//何か引数を受け取りたい場合は、ArgTypesに指定する。 using RetType = void; template <class T> void operator()(T& v, int f, int g) { for (auto& d : v) d += f, d *= g; } }; void FuncAnyRefGenerics1(Generics<std::tuple<AnyRef>, std::tuple<Visitor0_0, Visitor0_1, Visitor0_2>> a) { a.Visit<0>();//call Visitor0_0::operator() std::cout << a.Visit<1>() << std::endl;//call Visitor0_1::operator() a.Visit<2>(3, 2);//call Visitor0_2::operator() } void main() { std::vector<int> v{ 1, 2, 3, 4, 5 }; FuncAnyRefGenerics1(std::forward_as_tuple(v)); std::cout << "result of Visit0_2 with std::vector<int> =="; for (auto d : v) std::cout << " " << d; std::cout << std::endl; std::list<int> l{ 6, 7, 8, 9, 10 }; FuncAnyRefGenerics1(std::forward_as_tuple(l)); std::cout << "result of Visit0_2 with std::list<int> =="; for (auto d : l) std::cout << " " << d; std::cout << std::endl; std::string s = "12345"; FuncAnyRefGenerics1(std::forward_as_tuple(s)); std::cout << "result of Visit0_2 with std::string =="; for (auto d : s) std::cout << " " << d; std::cout << std::endl; }
example2 複数のAnyRefに対して一括して処理を呼び分ける場合。
struct Visitor1_0 { using ArgTypes = std::tuple<>; using RetType = void; template <class S, class T, class U> void operator()(const S& a, const T& b, U& res) const { res = a + b; } }; void FuncAnyRefGenerics2(Generics<std::tuple<AnyCRef, AnyCRef, AnyRef>, std::tuple<Visitor1_0>> a) { //足し算して結果をresに格納できるのなら、aにはどんな型の値を与えても良い。 //逆に言えば、足し算できない型(std::vectorとか)を与えればコンパイルエラーになる。 a.Visit<0>();//Visitor0の処理を呼び出す。 } void main() { int ires; FuncAnyRefGenerics2(std::forward_as_tuple(1, 2, ires)); std::cout << "result of Visit1_1 with int == " << ires << std::endl; std::string sres; FuncAnyRefGenerics2(std::forward_as_tuple(std::string("123"), "456", sres)); std::cout << "result of Visit1_1 std::string int == " << sres << std::endl; return 0; }
上のGenericsの実装では省略しているが、GitHubで公開している方ではもう少し凝った実装をして、可変長引数に対応させている。
struct Accumulable { using ArgTypes = std::tuple<std::ostream&>; using RetType = void; template <class ...T> void operator()(std::ostream& o, T&& ...v) const { o << "result of Accumulable::operator() with " << sizeof...(T) << " args = " << (... + v) << std::endl; } }; struct RuntimeVariadicGenericsBase { virtual ~RuntimeVariadicGenericsBase() = default; virtual void accumulate(Generics<Variadic<AnyURef>, Accumulable> a) const = 0; }; struct RuntimeVariadicGenericsDerived : public RuntimeVariadicGenericsBase { virtual void accumulate(Generics<Variadic<AnyURef>, Accumulable> a) const { a.Visit<0>(std::cout); } }; int main() { RuntimeVariadicGenericsBase* b = new RuntimeVariadicGenericsDerived(); b->accumulate(std::forward_as_tuple(std::string("abc"), "def", "ghi"));//足し算を行いたいので先頭だけはstd::stringにしている。 b->accumulate(std::forward_as_tuple(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));//整数10個。 delete b; return 0; } /*output result of Accumulable::operator() with 3 args = abcdefghi result of Accumulable::operator() with 10 args = 55 */
Genericsクラス自体は引数の型情報を要求しないので、仮想関数に対して持たせることももちろん可能である。これで私が行いたかったことはほぼ実現した。
与えた引数が処理の条件を満たさない時、単純にコンパイルエラーにせずSFINAEのように他のオーバーロード候補を探す、というような方法は、多分原理的に不可能ではないが、そこまで作り込んでいない。
私の貧弱な脳味噌では引数と戻り値の型を自前で定義したVisitorクラスを与えるという酷い方法しか作ることが出来なかったのが心残り。オーバーロードされた関数オブジェクトの共通する引数、戻り値の型を取得する方法とかあるんだろうか……。改善のアイディアがあればぜひ教えてほしい。
Visitorクラスはインスタンスではなく型を与える形になっている。ので、例えば何らかの初期化が必要なメンバを持つ関数オブジェクトなどは与えられない。頑張ればそのあたりも作れなくはないが面倒くさかったので止めた。ArgTypesとRetTypeを定義しなければならない時点でどうせラムダ式は使えないんだし。
各コンパイラでEmpty Base Optimizationが必ず働くことが保証されているのか詳しく知らないので、一応static_assertでバッファオーバーフローの可能性を検知させている。msvcの場合、昔検証した限りでは多重継承をしたときにEBOが働かないケースがあったので、直列に継承させている。根本的な対処はどうすれば良いんだろう。そういう意味で、この実装は割と怪しいことをしておりポータビリティに乏しいことは特筆すべきだろう。
こういう機能を実装した既知のライブラリというのは既にあっても良い気はするのだが、私が調べた限りでは見つからなかった。需要がないのだろうか。
そういう意味では久々に変態機能を作った気がする。今回は具体的な用途があったし既に利用しているが、次に使う機会は訪れるのだろうか。いや、まあ、テンプレート使いすぎてコンパイルすらままならない私のライブラリなら、きっと出番はあるはずだ。
まあ仮に出番がなかったとしても、久々にコーディングが楽しかったので満足である。
(後日、こんな面白い機能のライブラリが作られてないはずがないと思ってあちこち探し回ったところ、やっぱり細々と見つかった。Boost-ext::teとか、folly/Polyとか。そりゃそうだよな……)。
-
これは別に参照型で受け取ることとは関係ないのだが、私の用途で同時にこれらの要求が発生したので、纏めて作った。↩