2019年7月28日日曜日

7: C++スタンダードコンテナ互換のカスタムコンテナを作成する

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

スタンダード'map'、'list'、'set'、'vector'、'queue'、'stack'、'priority_queue'は、拡張することが非推奨とされている。えーと . . .、それで?

話題


About: C++

この記事の目次


開始コンテキスト


  • 読者は、C++とはどういうものであるかの基本的な知識を持っている、たとえ、一部の、難しいと考えられている要素を正確に理解していないとしても。

ターゲットコンテキスト



  • 読者は、C++においてスタンダードコンテナ互換のカスタムコンテナを作成する場合にどうするように想定されているかを理解する。

オリエンテーション


Hypothesizer 7
要素群の順序が維持される'map'が私は欲しい。要素が、指定した位置に挿入され、それに応じて取り出される'map'が。

スタンダード'map'はそういうマップではない。

私のマップは、スタンダード'map'に無関係なものであって欲しくなく、スタンダード'map'が想定されているところには使用できる一種のスタンダード'map'であって欲しい。

そこで、私の最初の、自然な(私は、オブジェクト指向プログラミングのパラダイムによればこれが自然だと思う)計画は、スタンダード'map'をパブリックに拡張するというものであった、なぜなら、そうすれば、このサブクラスであるマップは、スタンダード'map'ポインターまたはリファレンスである引数を持つ任意の関数に透過的に渡すことができるであろうから(大抵、そういう引数はポインターかリファレンスだ、なぜなら、値渡しでマップを渡すのは、一般的に、コストが高いから)。

しかし、多くの人々が、それは勧められないと言い、その内の一部は、それは決して許されないと強固に仰せられる。ふーむ...。その人達は、スタンダード'map'、というより、どのスタンダードコンテナもバーチャルデストラクタを持っていないのだと言う。うむ、それでは、確かに、問題がある。

しかし、それでは、どうするように私は想定されているのか?

一部の人々は、継承ではなく集約を使うべきだと言う。

...えーと、集約は私が抱える問題を解決しないでしょう?...私は私のマップをスタンダード'map'ポインターまたはリファレンス引数を持つ関数に渡さなければいけないのですよ。そんな集約クラスのインスタンスをそうした関数には渡せないのですよ、スタンダード'map'の子孫ではないのだから...

一部の人々は、プライベート継承を勧める。

プライベート継承も私が抱える問題を解決しません。そのような、スタンダード'map'をプライベートに継承したクラスのインスタンスを、スタンダード'map'ポインターまたはリファレンス引数を持つ関数には渡せないのですよ、真にスタンダード'map'の子孫ではないのだから...

結局、スタンダード'map'をパブリックに拡張するのが不可避のようだ、私が抱える問題を解決するには...

しかし、多くの人々は仰せられる、「決して、スタンダードコンテナをパブリックに継承するべきではない!」。

...それでは、どうすれば、私の問題を解決できるのか?


本体


1: スタンダードコンテナは、'継承をベースとしたオブジェクト指向プログラミング'の概念に基づいてデザインされておらず、'無名インターフェイスをベースとしたオブジェクト指向プログラミング'の概念に基づいてデザインされている


Hypothesizer 7
スタンダードコンテナ群がどのように使用されるように想定されているかを理解するには、それらのデザインの基になっている概念をまず理解しなければならない。

実のところ、スタンダードコンテナは、'継承をベースとしたオブジェクト指向プログラミング'の概念に基づいてデザインされておらず、'無名インターフェイスをベースとしたオブジェクト指向プログラミング'(私がたった今作り出した用語、この概念を表わす適切な(実体からかけ離れた名前でない)用語を世の中に見つけられないから)の概念に基づいてデザインされている。

'無名インターフェイスをベースとしたオブジェクト指向プログラミング'が何を意味するかを理解するために、1つの例を見てみよう。

@C++ ソースコード
#include <list>
#include <map>
#include <string>

			template <typename T> void namelessInterfaceBasedFunction (T const & a_map) {
				a_map.begin ();
				a_map.end ();
			}
			
			class ABeginEndClass {
				public:
					void begin () const;
					void end () const;
			};
			
			void ABeginEndClass::begin ()  const {
			}
			
			void ABeginEndClass::end () const {
			}
			
			void test () {
				namelessInterfaceBasedFunction (::std::map <string, string> { {string ("a"), string ("aa")}, {string ("b"), string ("bb")}});
				namelessInterfaceBasedFunction (::std::list <string> {string ("a"), string ("b")});
				namelessInterfaceBasedFunction (ABeginEndClass ());
			}

そのコードは、コンパイルでき、問題なく動く。'namelessInterfaceBasedFunction'という関数が要求するのは、その関数が引数オブジェクトに対して2つの関数、'begin'および'end'を呼べるということだけだ。引数オブジェクトに対して求められるその要件がインターフェイスであり、継承をベースとしたオブジェクト指向プログラミングパラダイムでは名前のついたインターフェイスとして定義されるのだが、そのコードでは、そのインターフェイスは、名前のついた明示的なインターフェイスとして定義されておらず、関数のコンテンツによって定義されている。そこで、そのインターフェイスを無名インターフェイスと私は呼んだ。

「振る舞いベース」のような用語は実体からかけ離れた用語であることに注意しよう、振る舞い(その2つの関数が何を行ない、何を行なわないか)は全くどうでもよいのだから。

実のところ、私はその引数をマップとして意図している('a_map'という名前が示唆するとおり)のだが、その関数は、リストや、コンテナでないオブジェクトでさえも受け入れてしまう。

私の元々の疑問は、「スタンダード'map'ポインターまたはリファレンスである引数を持つ関数にインスタンスを渡せるカスタムマップをどのように作成するように想定されているのか?」だったのだが、答えは、'そのような引数を設けるように想定されていない、そうした引数が、厳密にそのスタンダード'map'タイプであるインスタンスだけを受け入れるよう意図されている場合を除いては。必要であれば引数をテンプレートタイプにするように想定されている'だということになる。


2: それでは、私のスタンダード'map'互換カスタムマップはどのようなものであるべきなのか?


Hypothesizer 7
その意図されたパラダイムによれば、私のカスタムマップをスタンダード'map'の代わりにあらゆる可能な許されたケースで使用できるようにするには、私のカスタムマップは、スタンダード'map'のパブリック要素全て(メンバーだけでなくタイプも)に対して、対応するパブリック要素を同一シグネチャーで実装しなければならない。

「許された」と私が言った理由は、ある引数が明示的にスタンダード'map'タイプ(引数がポインターであろうがリファレンスであろうが)を使っていたら(テンプレートタイプではなく)、そこではカスタムマップは使用を許されないから。

私のカスタムマップがスタンダード'map'の代わりにある限られたケースだけで使用できればよいのであれば、私のカスタムマップは、その限られたケースで使用されるパブリック要素だけを、その限られたケースにフィットするように実装すればよい(そうした各要素は、必ずしも、対応するスタンダード'map'パブリック要素と同一シグネチャーを持たなくてもよい)。

カスタムマップは、スタンダード'map'インスタンス(複数かもしれない)をメンバーとして持ってもよいし、スタンダード'map'をプライベートに拡張してもよい(必要であれば、その上で、スタンダード'map'インスタンス(複数かもしれない)をメンバーとして持ってもよい)が、基本的に、スタンダード'map'をパブリックに拡張すべきでない(多くの人々が正しくも言っているとおり)。

勿論、対応するイテレータ群も同様に実装されなければならない。

まあ、多くの要素があるので、いくぶん面倒な作業を強いられるが、行なわなければならないことは明確だ。


3: 若干の苦情


Hypothesizer 7
スタンダードコンテナがどのように使われるように想定されているか、カスタムコンテナがどのようなものであるように想定されているか、を説明したが、それらを好ましく思っているとは私は言わなかった。正直言って、私は、それらがとても嫌いだ。

1つの問題は、一般的に言って、関数の引数に求められる無名インターフェイスの詳細を容易には知ることができないということだ: 関数の、場合によっては長いコンテンツを分析して、引数がどのように使用されているかを知らなければならない。あるクラスのインスタンスは、複数の関数に渡されるように意図されるので、そのクラスに対する要件は、そうした無名インターフェイスの全てをマージした後にのみ知ることができる。また、あるクラスが当該無名インターフェイスを実装しているかどうかも、容易には知ることができない: クラス定義を分析して、求められる要素の各々がそのクラスに実装されているかを確認しなければならない。

別の問題は、関数のコンテンツへの変更がその関数内に隔離されないことだ。私が言いたいのは、関数が名の付いた明示的インターフェイスの引数に基づいていたら、関数がその内部で引数をどのように使用しようとも、その関数の呼び出し元を壊しはしないので、その関数内の変更に私は専念できるということ(変更の影響を可能な限り小さい範囲に隔離することは、真っ当なプログラミング言語であればどれも、主要な関心事の1つとしていると私は考えてきた)。他方、もしも、関数が無名インターフェイス引数に基づいていたら、関数への変更が一部の呼び出し元を壊すかもしれない、そうした変更が無名インターフェイスを変えるかもしれないから。

実のところ、'無名インターフェイスをベースとしたオブジェクト指向プログラミング'は一種のいわゆる「ダックタイピング」だが、正直言って、私は「ダックタイピング」を忌み嫌っている。

「ダックタイピング」の熱烈支持者の一部は、「ダックタイピング」が変更作業を低減すると主張するが、私は疑問を持つ。私が言いたいのは、ある種のケースでは変更作業を低減するかもしれないが、変更作業は、変更がプログラムを壊さないことをチェックすることを含むのであって、1つの関数のコンテンツを変更するためだけのためにプログラム全体をチェックしなければならなくなる 「ダックタイピング」は低減された作業を必ずしも意味しないということ。

一部のプログラミング言語が「ダックタイピング」熱烈支持者をターゲットにするのは大いに結構だが(正直言って、自分で使用する意図のないプログラミング言語がいかにあろうとも、私には関心がない)、Cの強化版であるC++にはその方向には行ってもらいたくなかった。

実際には、なぜ、スタンダードコンテナが'継承をベースとしたオブジェクト指向プログラミング'を避けたかの正当な理由を私は理解する: パフォーマンス上のアドバンテージだ。確かに、実行時ポリモーフィズムはパフォーマンス上のコストがかかり、パフォーマンスはC++にとってとても重要だ。

しかしながら、私の意見では、名のついた明示的なインターフェイスをテンプレートタイプ引数に強制し、そのインターフェイスの要素のみがその関数内で使用できるように強制する方法がありえるし、あるべきだ。その方法では、パフォーマンス上の不利はないだろう: ポリモーフィズムは使用されない(テンプレートタイプ引数オブジェクトのメンバーたちはバーチャルである必要がない、具体的なサブクラス(インターフェイスではなく)がテンプレートタイプとして使われるから)。

ちゃんとしたドキュメントを書いたり、ちゃんとした変数名を付けたり、などしさえすれば、私が主張する問題を解決できると、主張する一部の人々がいるが、これを訊かせてほしい: 誰が本当にそんなちゃんとしたドキュメントやちゃんとしたソースコードを書いているのですか?...

実際、私は指摘せざるを得ない、世の中のほとんどの既存のソフトウェアドキュメントやソースコードはちゃんとしていない、と: 不適切な用語体系が使われ、非リーズナブルな説明が行われ、書いた者だけが理解できる(書いた者でさえもしばらく後には理解しないかもしれない)省略した名前が使われている。実際、多くのきちんとしていない広く流布している記述(権威者によるものであろうとあるまいと)(例えば、リファレンス「lvalue」、「rvalue」「xvalue」、「prvalue」、「glvalue」について)を私は本シリーズおよび他のいくつかのシリーズ(例えば、'あるオープンソースオフィススイートを活用する???', 'Javaを理解することをお許しください???', and 'Gitを理解することをお許しください')でこれまで指摘してきたし、これからもするだろう。

確かに、私が主張する問題は、"もしも"ちゃんとしたドキュメントかちゃんとしたソースコードが書かれれば、解決されるかもしれないが、実績に基づいて判断すれば、多くのちゃんとしたドキュメントやちゃんとしたソースコードが多くの人々によって書かれるだろうと期待するように自らを欺くことは私にはできない、特に、ちゃんとしたドキュメントを書くか、ちゃんとした変数名を付けるかしさえすれば、問題を解決できると涼しげに主張する人々によっては。


4: 結びとその先


Hypothesizer 7
これで、C++でスタンダードコンテナ互換のカスタムコンテナを作成する方法の基本概念を私は理解したようだ。

基本概念はシンプルであるが、スタンダード'map'およびそのイテレータ群は多くのパブリック要素を持っており、実際にカスタムマップを作成するのはそれほど容易ではない。

次記事で、要素群の順序が維持される'map'を作成する。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>