web-dev-qa-db-ja.com

効果的なC ++項目23メンバー関数よりも非メンバー非フレンド関数を優先する

クラスの設計、特に関数をメンバーにするかどうかについていくつかの事実に戸惑いながら、Effective c ++を調べたところ、項目23、つまり、メンバー関数よりも非メンバー非フレンド関数を優先することがわかりました。 Webブラウザーの例でそれを直接読むことは理にかなっていますが、その例の便利な関数(本ではこのような非メンバー関数と呼ばれています)はクラスの状態を変更しますね。

  • それで、最初の質問、彼らはメンバーになるべきではありませんか?

  • もう少し読んで、彼はSTL関数を検討し、実際、いくつかのクラスによって実装されていないいくつかの関数はstlに実装されています。この本のアイデアに従って、それらはalgorithmstd::sortstd::copyなどのいくつかの合理的な名前空間にパックされたいくつかの便利な関数に進化します。たとえば、vectorクラスにはsort関数がなく、stl sort関数を使用するため、ベクトルクラスのメンバーではありません。しかし、同じ推論をassignなどのベクトルクラスの他の関数に拡張して、メンバーとしてではなく便利な関数として実装することもできます。ただし、これにより、オブジェクトが操作された並べ替えなど、オブジェクトの内部状態も変更されます。それで、この微妙だが重要な(私が推測する)問題の背後にある理論的根拠は何ですか。

この本にアクセスできる場合は、これらの点をもう少し明確にしていただけますか?

35
Umut Tabak

本へのアクセスは決して必要ではありません。

ここで扱っている問題は依存関係再利用です。

適切に設計されたソフトウェアでは、依存関係を減らすためにアイテムを互いに分離しようとします。依存関係は、変更が必要なときに克服するためのハードルだからです。

適切に設計されたソフトウェアでは、[〜#〜] dry [〜#〜]の原則(Do n't Repeat Yourself)を適用します。これは、変更が必要な場合、苦痛を伴い、エラーが発生しやすいためです。十数か所でそれを繰り返さなければなりません。

「クラシック」OOマインドセットは、依存関係の処理がますます悪くなっています。クラスの内部に直接依存するメソッドをたくさん持つことで、わずかな変更は全体の書き直しを意味します。そう。

C++では、STL(標準ライブラリ全体ではない)は、次の明確な目標を持って設計されています。

  • 依存関係の削減
  • 再利用を許可する

したがって、コンテナは、内部表現を非表示にするが、アルゴリズムを実行できるようにカプセル化した情報への十分なアクセスを提供する、明確に定義されたインターフェイスを公開します。すべての変更は、不変条件が保証されるように、コンテナインターフェイスを介して行われます。

たとえば、sortアルゴリズムの要件について考える場合です。 STLで(一般的に)使用される実装の場合、(コンテナーから)次のものが必要です。

  • 特定のインデックスのアイテムへの効率的なアクセス:ランダムアクセス
  • 2つのアイテムを交換する機能:連想的ではない

したがって、ランダムアクセスを提供し、関連付けられていないコンテナは、(理論的には)クイックソートアルゴリズムによって効率的にソートするのに適しています。

これを満たすC++のコンテナは何ですか?

  • 基本的なCアレイ
  • deque
  • vector

そして、これらの詳細に注意を払えば、yoのコンテナが書き込む可能性があります。

それらのそれぞれについてsortを書き直す(コピー/貼り付け/微調整)のは無駄だと思いませんか?

たとえば、std::list::sortメソッドがあることに注意してください。どうして ? std::listはランダムアクセスを提供しないため(非公式にはmyList[4]は機能しません)、したがってsortfromアルゴリズムは適切ではありません。

37
Matthieu M.

私が使用する基準は、関数がメンバー関数であることによって大幅に効率的に実装できるかどうかです。その場合、それはメンバー関数である必要があります。 ::std::sortはその定義を満たしていません。実際、外部と内部の実装で効率の違いはまったくありません。

メンバー(またはフレンド)関数として何かを実装することで効率が大幅に向上するということは、クラスの内部状態を知ることで大きなメリットが得られることを意味します。

インターフェイス設計の技術の一部は、オブジェクトに対して実行する可能性のあるすべての操作をそれらの観点から合理的に効率的に実装できるように、最小限のメンバー関数のセットを見つける技術です。また、このセットは、クラスで実行してはならない操作をサポートするべきではありません。したがって、一連のゲッター関数とセッター関数を実装して、それを適切に呼び出すことはできません。

19
Omnifarious

このルールの理由は、メンバー関数を使用すると、誤ってクラスの内部に依存しすぎる可能性があるためだと思います。クラスの状態を変更することは問題ではありません。本当の問題は、クラス内のプライベートプロパティを変更する場合に変更する必要のあるコードの量です。クラスのインターフェース(パブリックメソッド)を可能な限り小さく保つことで、そのような場合に必要な作業量と、プライベートデータで何か奇妙なことをして、インスタンスが一貫性のない状態になるリスクの両方を減らすことができます。 。

AtoMerZも正しいです。非メンバー、非フレンド関数はテンプレート化して、他のタイプにも再利用できます。

ちなみに、Effective C++のコピーを購入する必要があります。これはすばらしい本ですが、この本のすべての項目に常に準拠しようとしないでください。オブジェクト指向設計は、(本などからの)優れた実践と経験(どこかでEffective C++でも書かれていると思います)の両方です。

11
ascobol

動機は単純です:一貫した構文を維持します。クラスが進化または使用されると、さまざまな非メンバーの便利な関数が表示されます。たとえば、toUpperのようなものを文字列クラスに追加するようにクラスインターフェイスを変更する必要はありません。 (の場合 std::stringもちろん、できません。)スコットの心配は、これが発生すると、構文に一貫性がなくなることです。

s.insert( "abc" );
toUpper( s );

無料の関数のみを使用し、必要に応じてフレンドと宣言することで、すべての関数の構文が同じになります。別の方法は、便利な関数を追加するたびにクラス定義を変更することです。

私は完全に確信しているわけではありません。クラスが適切に設計されていれば、基本的な機能があり、どの関数がその基本的な機能の一部であり、どの関数が追加の便利な関数であるか(存在する場合)はユーザーに明らかです。文字列は、さまざまな問題を解決するために使用されるように設計されているため、世界的には特殊なケースのようなものです。これが多くのクラスに当てはまるとは想像できません。

3
James Kanze

それで、最初の質問、彼らはより多くのメンバーであるべきではありませんか?

いいえ、これは続きません。慣用的なC++クラス設計(少なくとも、Effective C++で使用されるイディオム)では、非メンバーの非フレンド関数がクラスインターフェイスを拡張します。これらは、クラスへのプライベートアクセスを必要とせず、持っていないという事実にもかかわらず、クラスのパブリックAPIの一部と見なすことができます。この設計がOOPの定義によって「OOPではない」場合、OK、慣用的なC++はその定義によってOOP)ではありません。

同じ推論をベクトルクラスの他のいくつかの関数に拡張します

確かに、標準コンテナには、無料の関数である可能性のあるいくつかのメンバー関数があります。例えば ​​vector::Push_backinsertで定義されており、クラスへのプライベートアクセスなしで実装できます。ただし、その場合はPush_backは、ベクトルが実装する抽象的な概念BackInsertionSequenceの一部です。このような一般的な概念は特定のクラスの設計にまたがっているため、関数を配置する場所に影響を与える可能性のある独自の一般的な概念を設計または実装している場合。

確かに、おそらく異なるはずの標準の部分があります。たとえば、 std :: stringのメンバー関数が多すぎます 。しかし、行われたことは行われ、これらのクラスは、人々が現在現代のC++スタイルと呼ばれるものに実際に落ち着く前に設計されました。クラスはどちらの方法でも機能するので、違いを心配することで得られる実用的なメリットはそれほど多くありません。

3
Steve Jessop

さまざまな考え:

  • 非メンバーがクラスのパブリックAPIを介して作業する場合、次のようなコードの量が削減されるので便利です。
    • クラスの不変条件を確認するために注意深く監視する必要があります。
    • オブジェクトの実装を再設計する場合は、変更する必要があります。
  • それだけでは不十分な場合でも、非メンバーをfriendにすることができます。
  • メンバーは暗黙的にスコープ内にないため、非メンバー関数を作成することは通常、あまり便利ではありませんが、プログラムの進化を考慮すると:
    • 非メンバー関数が存在し、同じ機能が他の型にも役立つことがわかったら、関数をテンプレートに変換して、両方の型だけでなく、将来の任意の型でも使用できるようにするのは一般に非常に簡単です。言い換えると、非メンバーテンプレートでは、実行時のポリモーフィズム/仮想ディスパッチよりもさらに柔軟なアルゴリズムの再利用が可能です。テンプレートでは、 ダックタイピング と呼ばれるものが可能です。
    • 便利なメンバー関数を備えた既存のタイプは、関数をreに変換するほとんどの方法があるため、類似の動作を希望する他のタイプにカットアンドペーストを推奨します。 -use requireは、すべての暗黙的なメンバーアクセスを特定のオブジェクトへの明示的なアクセスにする必要があります。これは、プログラマーにとって30秒以上の退屈な作業になります。
  • メンバー関数を使用すると、object.function(x, y, z)表記が可能になります。これは、IMHOが非常に便利で、表現力があり、直感的です。また、多くのIDEの検出/完了機能との連携も優れています。
  • メンバー関数と非メンバー関数としての分離は、クラスの本質的な性質、その不変条件と基本的な操作を伝達し、アドオンと場合によってはアドホックな「便利な」機能を論理的にグループ化するのに役立ちます。 TonyHoareの知恵を考えてみましょう。

    「ソフトウェア設計を構築する方法は2つあります。1つは明らかに欠陥がないように単純にする方法、もう1つは明らかな欠陥がないように複雑にする方法です。方法ははるかに難しいです。」

    • ここでは、非メンバーの使用は必ずしもそれほど難しいわけではありませんが、メンバーデータとプライベート/保護されたメソッドにアクセスする方法とその理由、およびどの操作が基本であるかについてもっと考える必要があります。このような魂の検索は、メンバー関数を使用したデザインも改善します。怠惰になるのは簡単です:-/。
  • 非メンバー機能が高度に拡張されたり、追加の依存関係を取得したりすると、関数を個別のヘッダーや実装ファイル、さらにはライブラリに移動できるため、コア機能のユーザーは必要な部分の使用に対してのみ「料金」を支払うことになります。

(Omnifariousの答えは必読です。初めての場合は3回です。)

3
Tony Delroy

ソートはベクトルだけでなく広く使われているので、メンバー関数として実装されていないと思います。メンバー関数として持っている場合は、それを使用するコンテナーごとに毎回再実装する必要があります。ですから、実装を簡単にするためだと思います。

1
atoMerz