›C++【C++】既存のクラスを拡張する方法【拡張メソッド/カテゴリ】C#の拡張メソッドやRubyのオープンクラス、Swiftのextension、Objective-Cのカテゴリーの活用例に近い感覚のものを実現させます。既存のクラスにメソッド(メンバ関数)を追加する方法と関数(フリー関数)で代替する方法、演算子の多重定義を活用する方法の3種類の方法を紹介します。フリー関数(例:print(std::string("s"));)演算子関数(例:std::string("s")-print;)メンバ関数(例:std::string("s").print();)オススメはフリー関数版の利用です。その他のイディオムは扱いが難しく、実用性には難があります。フリー関数もっとも簡単で手軽な方法です。フリー関数の第一引数にオブジェクトの参照を渡し、その参照値を元に関数内で独自処理を記述していきます。std::endl;}同一の名前空間で同名のprint関数が複数定義されていても問題はありません。C++の厳格な型検査と関数のオーバーロード解決の仕組みよって、型に応じた関数呼び出しが行われます。以下はC++の文字列クラスを対象に拡張用の関数を複数定義した例です。"12"統一関数呼び出し記法が実用化された場合、将来的にメソッド呼び出し形式での利用が可能になる可能性があります。既存クラス向けの演算子を多重定義し、オブジェクトに対して関数オブジェクトを適用するテクニックです。演算子によるメソッドチェーンも実現できます。この発想は入出力ストリームの<<演算子の活用例と似たようなものだと思ってください。演算子の優先順位や重複定義にも注意する必要があります。今回紹介する方法は通常の継承や別名クラスを用いた方法とは違い、クラス名や名前空間の名称を変える必要がないという特徴があります。"abc";str.print();StringUtils::print(str);1のように既存のstd::stringクラスにprint関数を直接追加するイメージです。名前空間を継承するstd等の既存の名前空間を継承し、その中でstringクラス等の既存クラスを継承することで、事実上のクラス拡張を実現させます。std::coutも使える}クラスのクローンを作るようなイメージですね。std::string::string;は基底クラスのコンストラクタを継承するために必要です。これにより派生クラスに対して、基底クラスと同等の振る舞いをさせることができます。extensions;は継承された拡張空間extensionsで既存のstdを上書きするために利用しています(正確にはエリアスとしてローカル宣言している)。main関数の外(グローバル空間)では利用できませんが、名前空間内やメンバ関数内では利用可能です。以下は実際にstd::stringクラスにprintメンバ関数を追加するサンプルコードです。"abc"}間接的な利用テクニック場合によっては、拡張クラスを戻り値にした場合に、暗黙の型変換によって自動的に基底クラスへのアップキャスト行われてしまう場合がありますが、autoで受け取ることでこれを回避できます。'std::__1::basic_string<char>'}これによって、拡張クラスを適用していない空間でも、拡張機能の間接的な利用が可能になります。メンバ重複の対処とカテゴライズ既存の機能にバリエーションを持たせる技法も合わせて紹介しておきます。同じメンバ名で異なる挙動を持たせる事が可能になります。標準ライブラリのクラス向けに、「ADL」を前提とした関数を定義することも可能となります。今回紹介したこの技法を用いると、stdに自作クラスを追加することが可能になってしまいます(あくまで仮想的・間接的ではありますが)。std::jsonやstd::string_viewという機能を独自に追加できてしまうわけです。C++の規格上では名前空間stdに特殊化以外の用途で独自の定義を加えることが禁止されているのですが(17.4.3.1)、今回の方法であれば、おそらくその制限には抵触しない形になるのではないかと思われます。そもそも名前空間のエイリアスにstdを使っていいのかという疑問も残ります。常識の範囲内で考えても良いイディオムではないかもしれません。b)式と解釈させる仕様がC++に取り込まれた場合、今回紹介したテクニックは不要になり、代わりに以下のような書き方で、既存クラスの拡張が実現できるようになります。代わりにこちらの関数呼び出しが行われるprint(std::string("a"));この未来、信じるか信じないかはあなた次第です。今回紹介した技法はかなりアクロバティックな技法であり賛否両論があるかと思います。個人的には移植性の問題を気にしなければ問題なく使えるレベルのものだと感じています。他にも注意しなければならない問題は色々あるのですが、その点については後半でじっくり解説します。デバッグ用途での使用がオススメちなみに、この技法はもともとstd::vector::operator[]演算子の境界チェックを実現するためのテクニックでもあったため、デバッグ用途で使うのが本来もっともオススメの活用方法です。後々面倒なやり方こうすることで、境界チェック(範囲チェック)用のクラスを別名で作る必要が無くなります。リリースビルド前のコード修正や、下手なマクロ置換も不要になります。実用化は難しいstdを再定義する際に一つの名前空間しか指定できないため、モジュール化や個々の機能のカテゴライズが困難だという実用面の問題が残っています。用途によっては、多態性の仕組みや非virtualデストラクタの問題をきちんと理解した上で使わなければなりません。今回の技法は結局のところクラスの継承を用いているため、「非仮想デストラクタ」問題の影響を受けます。つまるところ、アップキャスト後のポインタを解放した場合、派生クラス側のデストラクタが呼ばれなくなる問題に遭遇します。そのため、リソース解放が必要な処理には注意する必要があります。ダウンキャスト・アップキャストの問題C++では基底クラスから派生クラスへの暗黙的なキャストが行われないため、手動でのキャストを行う必要が出てきます。よって、他のAPIとのやり取りがしづらくなるという欠点があります。公開インターフェースに関する問題は以下の方法で対処できます。拡張機能の利用を隠蔽することも可能になります(つまりmain.cpp側は拡張クラスやキャスト処理を意識する必要がなくなる)。内部的な問題に関しては、コピー・コンストラクタを手動で宣言することで対処が可能ですがオススメはしません。}};しかし、そこまでして本イディオムを使うべきかどうかは疑問があります。本イディオムは、デバッグ用のイディオムとしての利用が適切だと考えており、あくまで「簡易的な機能」として割り切って使われるべきでしょう。既存のAPIをフックして不具合の原因特定を行ったり、デバッガ用に動的な関数(debugDescription等)を追加したりと、開発効率の向上や不具合の原因調査を目的に活用するのがオススメです。メンバ関数を追加する必要が出てきます。本来であれば、string_printer.hppやstring_regex.hppというヘッダーファイルを作成し、必要に応じて両者をインポートしたり、組み合わせて利用できるようにしたいのですが、現状の言語仕様ではその手の実現が困難です。プリプロセッサを用いればできなくもないのですが、それはあくまで最終手段です。}という入れ子状の宣言方法を用いれば、ファイル分割自体は可能になりますが、両者の間に依存関係が形成されてしまいます。結局のところ、モジュール化という本来の目的は果たせません。クラス別のモジュール化が困難また同様に、stringクラス用の拡張とvectorクラス用の拡張を分離するといったことも困難です。x;}対応策としては、以下のような複数の方法が考えられます。コード単位でモジュール化パッケージ管理や保守、単体テストがしづらくなる問題があります。そのため、場合によってはプリプロセッサの利用が必要になるかもしれません。marge;}共通の名前空間を決め打ちしてモジュール化おそらく最も現実的な妥協案です。std_neue;}現実的とは言っても問題は残ります。固定的な名前を使っているため、自作した拡張名前空間の内の一部の機能だけを、第三者の物とすり替えるようなことができなくなります。他人のライブラリでも同様にstd_neueという名前空間が使われていれば、この限りではありませんが・・・。コード単位でモジュール化する方法を採る必要が出てきます。名前空間の侵食問題またstring拡張とvector拡張で共通の名前空間を使っているため、「片方の拡張を使うと、もう一方の拡張も取り込まれてしまう」という問題もあります。vector拡張のみ使いたいがstring拡張も取り込まれてしまう}ヘッダ側がリンケージを持たず、また多重インクルードを容認する前提であれば、以下の方法での対処も可能です。例外的な活用法であることに注意してください。vector拡張のみ取り込まれる}結局のところ実用的ではない以上のことから、本イディオムには、保守性とユーザビリティを担保するための最善の策が存在しないという重大な問題があります。C++の今後の進化によっては本イディオムが実用化できる日がくるかもしれません。そもそも既存クラスの拡張はきちんと言語機能としてサポートして欲しい気もします。既存のC言語APIも扱いやすくなるので、個人的には面白い仕様だと感じています。オートコンプリートの恩恵も受けられるようになります。template余談ですが、もし仮に名前空間でテンプレート機能が利用できるようになれば、以下のように、利用シーンに応じたモジュール選択が可能になるかもしれません。"a,b,c,"}このように必要な拡張をチェーン状に繋げていくイメージですね。モジュールの取り回しや管理はしやすくなりそうです。extension_map_xml<std>>>>;えげつないですね。templateが実現されれば、先程挙げた様々な問題が一気に解消されます。