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


12

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

// 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 ++を学ぶ)、これは非常に良いことのように思えます。ヘッダーがインターフェースを定義し、.cppが実装を定義しているためです(コンパイル時間の改善は素敵なボーナス)。ただし、そのように使用することを意図していない言語機能を悪用しているような匂いもします。それで、どちらですか?これは、プロのC ++プロジェクトで見たときに眉をひそめるものですか?
  3. 私が考えていない落とし穴はありますか?

Pimplを知っています。これは、ライブラリの端に実装を隠すはるかに堅牢な方法です。これは、クラスを値として扱う必要があるために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ます。


2
消費者は独自のクラスPredicateList_HelperFunctionsを定義し、プライベートフィールドにアクセスさせることができます。」それはODR違反ではないでしょうか?あなたと消費者の両方が同じクラスを定義する必要があります。これらの定義が等しくない場合、コードの形式は正しくありません。
ニコルボーラ

回答:


13

あなたがすでに認識していることを控えめに言っても少し難解です。あなたが何をしているのか、これらのヘルパークラスがどこで実装されているのか疑問に思ってコードに最初に遭遇したとき、私はちょっと頭を掻くかもしれません/ habits(この時点で完全に慣れる可能性があります)。

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

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

また、コード内に多くの「ヘルパー」が表示されると、少ししびれます。常に真実とは限りませんが、コードの重複を排除するために関数を単に自由に分解する開発者の兆候となる場合があります。他のいくつかの機能を実装するために必要なコード。クラスの実装をより多くの関数に分解する方法に関して、少しだけ少し前もって考えると、より明確になり、内部に完全にアクセスしてオブジェクトのインスタンス全体を渡すのに特定のパラメーターを渡す方が有利になる場合がありますデザイン思考のそのスタイルを促進します。もちろん、あなたがそれをしていることを提案しているわけではありません(私にはわかりません)。

それが扱いにくい場合、私は2番目の、より慣用的な解決策であるピンプルを検討します(あなたはそれに関する問題に言及したことを理解していますが、最小限の労力でそれらを避けるために解決策を一般化できると思います)。これにより、クラスの実装に必要な多くの情報を、プライベートデータを含め、ヘッダーホールセールから遠ざけることができます。本格的なユーザー定義コピークターを実装することなく、値のセマンティクスを保持しながら、ピンリストのパフォーマンスの問題を、フリーリストのようなダート安い定時間アロケーター*で大幅に軽減できます。

  • パフォーマンスの面では、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);
}

今ではそれは非常に意味のない違いのように見えるかもしれませんが、私はそれを「ヘルパー」と呼びます(クラスの内部状態全体を必要とするかどうかに関係なく関数に渡しているため、おそらく軽de的な意味で)が発生する「衝撃」要因を回避することを除いてfriend。一般friendに、クラス内部は他の場所からアクセス可能であると言われているため、頻繁にさらなる検査がないことを見ると少し怖いようです(これは、独自の不変式を維持できない可能性があるという意味を持ちます)。あなたが使用しfriendている方法では、人々が練習を知っている場合、それはむしろ意味がありませんfriendクラスのプライベート機能を実装するのに役立つ同じソースファイルに常駐していますが、上記の方法は、少なくともその種を回避する友人が関与しないという議論の余地のある利点でほぼ同じ効果を達成します( "Oh撮影、このクラスには友人がいます。プライベートはどこでアクセス/変更されますか?)。一方、すぐ上のバージョンは、の実装で行われたもの以外ではプライベートにアクセス/変更する方法がないことをすぐに伝えます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_implfind_detail、またはfind_helper、その後、私はまた、同様の方法でうんざり。

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

会費を避ける:可能な場合は、非会員以外の行事を行うことをお勧めします。[...]非メンバーの非フレンド関数は、依存関係を最小化することでカプセル化を改善します。関数の本体は、クラスの非パブリックメンバーに依存することはできません(項目11を参照)。また、モノリシッククラスを分解して分離可能な機能を解放し、結合をさらに減らします(項目33を参照)。

また、可変範囲を狭めるという基本原則の観点から、彼がある程度話している「会費」を理解することもできます。最も極端な例として、プログラム全体を実行するために必要なすべてのコードを備えた神オブジェクトを想像すると、すべての内部にアクセスできるこの種の「ヘルパー」(関数、メンバー関数または友人)クラスのprivates)は、基本的にこれらの変数をグローバル変数と同様に問題なくレンダリングします。この最も極端な例では、状態とスレッドの安全性を管理し、グローバル変数で得られる不変条件を維持するという困難がすべてあります。そしてもちろん、ほとんどの実際の例はこの極端に近い場所ではないことを願っていますが、情報の隠蔽はアクセスされる情報の範囲を制限するのと同じくらい便利です。

さて、サッターはすでにここで良い説明をしていますが、機能を設計する方法に関して、デカップリングは心理的な改善のように促進する傾向があります(少なくとも脳が私のように機能する場合)。クラスのすべてにアクセスできない関数を設計し始めるとき、それを渡す関連パラメーターのみ、またはクラスのインスタンスをパラメーターとして、そのパブリックメンバーのみを渡す場合、それは好む設計マインドセットを促進する傾向があります分離に加えて、カプセル化の改善を促進することに加えて、すべてにアクセスできる場合に設計するように誘惑されるかもしれないものよりも明確な目的を持つ関数。

極限に話を戻すと、グローバル変数にあふれたコードベースは、明確で一般化された目的で機能を設計するように開発者を正確に誘惑しません。関数でより多くの情報にアクセスできるようになると、より多くの人間がそれを非一般化し、その関数のより具体的で関連性のあるパラメーターを受け入れる代わりに、このすべての追加情報にアクセスするためにその明確さを減らす誘惑に直面します国家へのアクセスを制限し、適用範囲を広げ、意図の明確さを改善する。それはメンバー関数または友人に(一般にある程度ではありますが)適用されます。


1
入力いただきありがとうございます!ただし、この部分であなたがどこから来たのかは完全にはわかりません。「コード内に多くの「ヘルパー」が表示されると、時々少しうんざりします。」-すべてのプライベートメンバー関数は定義上ヘルパー関数ではありませんか?これは、一般にプライベートメンバー関数で問題になるようです。
ロバートフレイザー

1
ああ、内部クラスには「友人」はまったく必要ないので、そのようにすると、「友人」というキーワードが完全に回避されます
ロバートフレイザー

「すべてのプライベートメンバー関数は定義上ヘルパー関数ではありませんか?これはプライベートメンバー関数全般に問題があるようです。」それは最大のものではありません。自明ではないクラスの実装には、多数のプライベート関数またはすべてのクラスメンバーに一度にアクセスできるヘルパーが必要であると考えていました。しかし、私はLinus Torvalds、John Carmackのような偉大な人のスタイルを見ました。前者はCでコーディングしていますが、彼はオブジェクトの類似物をコーディングするとき、彼は一般的にCarmackと一緒にコーディングしません。
ドラゴンエナジー

そして当然、ソースファイルのヘルパーは、クラスの実装を支援するために多くのプライベート関数を使用したため、必要以上の外部ヘッダーを含む大規模なヘッダーよりも好ましいと思います。しかし、上記およびその他のスタイルを研究した後、クラスのすべての内部メンバーへのアクセスを必要とするタイプよりも少し一般化された関数を書くことがしばしば可能であることに気付きました。関数に適切な名前を付けて、機能するために必要な特定のメンバーを渡すと、多くの場合、時間を節約できます[...]
Dragon Energy

[...]それが取るよりも、全体的に明確な実装をもたらし、後で操作しやすくなります。あなたののすべてにアクセスする「完全一致」のための「ヘルパー述語」を書く代わりにPredicateList、述語リストからアクセスする必要のない少し一般化された関数に単にメンバーまたは2を渡すことが実行可能です。のすべてのプライベートメンバーPredicateList、および多くの場合、その内部機能に対してより明確でより一般化された名前と目的をもたらす傾向があり、「後知恵コードの再利用」の機会が増えます。
ドラゴンエナジー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.