web-dev-qa-db-ja.com

フレンドクラスを使用して、C ++でプライベートメンバー関数をカプセル化する-良い習慣または乱用?

だから私は次のようなことをすることでヘッダーにプライベート関数を置くことを避けることが可能であることに気づきました:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

プライベート関数はヘッダーで宣言されることはなく、ヘッダーをインポートするクラスのコンシューマーは、その存在を知る必要はありません。これは、ヘルパー関数がテンプレートである場合に必要です(代わりの方法は、完全なコードをヘッダーに入れることです)。これが、私がこれを「発見」した方法です。プライベートメンバー関数を追加/削除/変更した場合に、ヘッダーを含むすべてのファイルを再コンパイルする必要がないというもう1つのいい面があります。すべてのプライベート関数は.cppファイルにあります。

そう...

  1. これは名前のある有名なデザインパターンですか?
  2. 私(Java/C#のバックグラウンドから来て、自分の時間にC++を学ぶ)にとって、これは非常に良いことのように思われます。素敵なボーナス)。ただし、そのように使用することを意図していない言語機能を悪用しているようなにおいもします。それで、それは何ですか?これは、プロのC++プロジェクトで見たときに眉をひそめるものですか?
  3. 私が考えていない落とし穴はありますか?

私はPimplを知っています。これは、ライブラリEdgeで実装を非表示にするはるかに堅牢な方法です。これは、クラスを値として扱う必要があるため、Pimplがパフォーマンスの問題を引き起こすか、機能しない内部クラスで使用するためのものです。


編集2:以下のドラゴンエナジーの優れた答えは、friendキーワードをまったく使用しない次の解決策を提案しました:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

これは、同じ分離原理を維持しながら、friendの衝撃因子(gotoのように悪魔化されているように見える)を回避します。

12
Robert Fraser

あなたが何をしているのか、そしてあなたのスタイルを取り始めるまでこれらのヘルパークラスがどこに実装されているのかと思って最初にコードに遭遇し始めた瞬間に頭を悩ませるかもしれないことをすでに認識しているので、控えめに言っても少し難解です/習慣(その時点で私はそれに完全に慣れるかもしれません)。

ヘッダーの情報量を減らすのが好きです。特に非常に大規模なコードベースでは、コンパイル時の依存関係を減らし、最終的にビルド時間を短縮する実用的な効果があります。

しかし、私の直感的な反応は、このように実装の詳細を非表示にする必要があると感じた場合は、ソースファイル内の内部リンケージを持つ独立型関数にパラメーターを渡すことを好むということです。通常、クラスのすべての内部にアクセスせずに特定のクラスを実装するのに役立つユーティリティ関数(またはクラス全体)を実装できます。代わりに、メソッドの実装から関数(またはコンストラクタ)に関連する関数を渡すだけです。そして当然、それはあなたのクラスと「ヘルパー」の間の結合を減らすというボーナスを持っています。また、複数のクラスの実装に適用可能なより一般化された目的を提供し始めた場合、「ヘルパー」であったものをさらに一般化する傾向があります。

また、コードに多くの「ヘルパー」が含まれていると、ときどき少ししつこくなります。必ずしもそうとは限りませんが、場合によっては、関数を分解する開発者の症状を示すことがあり、膨大なデータの塊が関数に渡され、理解しにくい名前/目的を持つ関数に渡されて、それらの量を減らすという事実を超えて、コードの重複を排除します他のいくつかの機能を実装するために必要なコード。少しだけ前もって考えておくと、クラスの実装をさらに関数に分解する方法がはるかに明確になる場合があります。また、特定のパラメーターを渡すことで、内部へのフルアクセスでオブジェクトのインスタンス全体を渡すことができます。そのスタイルのデザイン思考を促進します。もちろんあなたがそうしていることを示唆しているわけではありませんが(私にはわかりません)、「ヘルパー」に満足しすぎたときにその傾向に注意する必要があるかもしれません。

それが扱いにくくなった場合は、2番目のより慣用的な解決策を検討します(これは問題を指摘していますが、最小限の労力で解決策を一般化して回避できると思います)。これにより、プライベートデータを含む、クラスに実装する必要がある多くの情報をヘッダーホールセールから移動できます。完全なユーザー定義のコピーctorを実装する必要なく値のセマンティクスを維持しながら、フリーリストのようなダートチープな一定時間アロケーター*を使用して、pimplのパフォーマンスの問題を大幅に軽減できます。

  • パフォーマンスの面では、pimplは少なくともポインタのオーバーヘッドを導入しますが、実際的な問題が発生する場合は、かなり深刻なケースになると思います。アロケータによって空間的な局所性が大幅に低下しない場合でも、オブジェクトを反復処理するタイトループ(パフォーマンスがそれほど重要でない場合は、一般に均一でなければなりません)でも、次のようなものを使用すると、キャッシュミスを最小限に抑える傾向があります。 pimplを割り当てるための空きリスト。クラスのフィールドをほぼ連続したメモリブロックに配置します。

個人的には、それらの可能性を使い果たした後にのみ、私はこのようなものを考えるでしょう。代替案がヘッダーに公開されたよりプライベートなメソッドのようなものであり、おそらくその難解な性質のみが実用的な問題である場合、それはまともな考えだと思います。

代替

友人がいないときに同じ目的をほぼ達成するちょうど今私の頭に浮かんだ1つの代替案は次のとおりです。

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

これは非常に悲観的な違いのように思えるかもしれませんが、私はそれを「ヘルパー」と呼んでいます(クラスの内部状態全体がすべて必要かどうかにかかわらず、関数に渡されているため、中傷的な意味で)。ただし、friendに遭遇する「ショック」要因を回避します。一般的にfriendは、クラスの内部には他の場所からアクセスできると記述されているため(独自の不変式を維持できない可能性があることを意味するため)、それ以上の検査がないことを確認するのは少し怖いです。 friendはクラスのプライベート機能を実装するのに役立つ同じソースファイルに存在するだけなので、慣れていることを知っていると、friendの使用方法はかなり悲観的になりますが、上記は、同じ種類の効果を少なくともほぼ同じ効果で実現しますが、そのすべての種類を回避する友達を含まないというおそらく議論の余地のない利点があります(「ああ、このクラスには友達がいます。プライベートはどこでアクセス/変更されますか?」 )。一方、すぐ上のバージョンでは、PredicateListの実装以外でプライベートにアクセス/変更する方法がないことをすぐに伝えます。

これはおそらく、このレベルのニュアンスを備えたやや独断的な領域に向かっていると思います。なぜなら、*Helper*に一律に名前を付ければ、すべてが同じソースファイルに入れられ、すべてがプライベートの一部としてバンドルされているので、誰でもすぐにわかるからです。クラスの実装。しかし、私たちがひどく不規則になったら、おそらくすぐ上にあるスタイルは、少し怖いように見えるfriendキーワードがないと、ひと目でそれほどひどい反応を引き起こさないでしょう。

その他の質問:

コンシューマーは独自のクラスPredicateList_HelperFunctionsを定義し、プライベートフィールドにアクセスさせることができます。私はこれを大きな問題とは見なしていませんが(これらのプライベートフィールドで本当にキャストしたい場合は、キャストを行うことができます)、消費者にそれをそのように使用することを奨励しますか?

これは、クライアントが同じ名前の2番目のクラスを定義し、リンケージエラーなしで内部にアクセスできるというAPI境界を越えた可能性があります。次に、私は主にグラフィックスで作業しているCコーダーです。このレベルの「what if」の安全性の懸念が優先リストで非常に低いため、これらのような懸念は私が手を振ってダンスをし、存在しないふりをしてみてください。 :-Dもしあなたがこれらのような懸念がかなり深刻であるドメインで働いているなら、私はそれを作るのにまともな考慮だと思います。

上記の代替案は、この問題に苦しむことも回避します。それでもfriendの使用に固執したい場合は、ヘルパーをネストされたプライベートクラスにすることで、この問題を回避することもできます。

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

これは名前のある有名なデザインパターンですか?

私の知る限りではありません。それは実装の詳細とスタイルの細部に本当に入り込んでいるので、ある種の疑いがある。

"ヘルパー地獄"

多数の「ヘルパー」コードを含む実装を見ると、時々どのように問題が発生するかについて、さらに説明を求められました。いくつかの問題については少し議論の余地があるかもしれませんが、実際にいくつかのデバッグ中に問題が発生したため、事実です。たくさんの「ヘルパー」を見つけるためだけの私のクラスの実装の私の同僚の。 :-Dそして、これらすべてのヘルパーが正確に何をすることになっているのかを理解しようとして頭を掻いたのは、私だけではありませんでした。 「ヘルパーを使わないでください」のように独断的になりたくありませんが、考えるのに役立つかもしれないという小さな提案をします欠けているものを実用的に実装する方法について。

すべてのプライベートメンバー関数ヘルパー関数は、定義上ではありませんか?

そして、はい、私はプライベートメソッドを含めています。単純なパブリックインターフェイスに似ているが、find_implまたはfind_detailまたはfind_helperのような目的でやや不適切に定義されている無限のプライベートメソッドセットのようなクラスを見つけた場合も、似たような方法で。

代替として私が提案しているのは、少なくとも「他の人を実装するのに役立つ関数」よりも一般化された目的でクラスを実装するのに役立つ、内部リンケージ(staticまたは匿名の名前空間内で宣言)を持つ非メンバーの非フレンド関数です。 。そして、私はここでC++ 'コーディング標準'からHerb Sutterを引用できますが、それが一般的なSEの観点から好ましい理由である理由は次のとおりです。

会費を避ける:可能な場合は、機能を会員以外の非友人にすることを優先します。 [...]非メンバーの非フレンド関数は、依存関係を最小限に抑えることでカプセル化を改善します。関数の本体は、クラスの非パブリックメンバーに依存することができなくなります(アイテム11を参照)。また、モノリシッククラスを分解して分離可能な機能を解放し、カップリングをさらに削減します(項目33を参照)。

また、変数の範囲を狭めるという基本原則に関して、彼がある程度話している「会費」を理解することもできます。最も極端な例として、プログラム全体を実行するために必要なすべてのコードを持つGodオブジェクトを想像すると、すべての内部にアクセスできるこの種類の「ヘルパー」(関数、メンバー関数かフレンドか)が優先されます(クラスのプライベート)は、基本的にこれらの変数をグローバル変数と同じくらい問題のあるものにします。状態とスレッドの安全性を管理し、この最も極端な例のグローバル変数で得られる不変条件を維持することは、すべて困難です。もちろん、実際の例のほとんどはこの極端に近いものではないでしょうが、情報の非表示は、アクセスされる情報の範囲を制限するのと同じくらい有用です。

ここでSutterはすでにここで素晴らしい説明をしていますが、機能の設計方法に関して、デカップリングが心理的な改善(少なくとも私の脳のように機能する場合)のように促進する傾向があることも付け加えておきます。渡す関連するパラメーターのみを除く、またはクラスのインスタンスをパラメーターとして渡す場合、そのパブリックメンバーのみを除く、クラスのすべてにアクセスできない関数の設計を開始すると、設計の考え方を促進する傾向があります。デカップリングとカプセル化の改善に加えて、すべてにアクセスできる場合に設計するよりも明確な目的を持つ関数。

極限に戻ると、グローバル変数で処理されたコードベースは、開発者が目的を明確かつ一般化された方法で関数を設計するように誘惑するわけではありません。関数内でより多くの情報にアクセスできるほど、私たちの多くの人は、関数をより具体化して関連性のあるパラメーターを受け入れる代わりに、私たちが持っているこのすべての追加情報にアクセスすることを優先して、それを一般化してその明快さを減らす誘惑に直面します。国家へのアクセスを狭め、適用範囲を広げ、意図の明確さを改善する。それは(一般的にはある程度は低いものの)会員の機能や友人にも当てはまります。

13
Dragon Energy