54

publicC++ には、誰でも呼び出すことができるメンバーと、すべてのメンバーを特定の外部クラスまたはメソッドに公開するが、特定のメンバーを特定の呼び出し元に公開するための構文を提供しないfriend宣言があるのはなぜですか? private

私は、既知の呼び出し元によってのみ呼び出されるいくつかのルーチンとのインターフェースを表現したいと考えています。それらの呼び出し元に、すべてのプライベートへの完全なアクセス権を与える必要はありません。私が思いつくことができる最高のもの(以下)と他の人による提案は、さまざまな間接性のイディオム/パターンを中心に展開しています。ここで、どの呼び出し元を明示的に示す単一の単純なクラス定義を持つ方法が本当に欲しいだけです(よりも細かく)、私の子供、または絶対に誰でも)どのメンバーにアクセスできますか。以下の概念を表現する最良の方法は何ですか?

// Can I grant Y::usesX(...) selective X::restricted(...) access more cleanly?
void Y::usesX(int n, X *x, int m) {
  X::AttorneyY::restricted(*x, n);
}

struct X {
  class AttorneyY;          // Proxies restricted state to part or all of Y.
private:
  void restricted(int);     // Something preferably selectively available.
  friend class AttorneyY;   // Give trusted member class private access.
  int personal_;            // Truly private state ...
};

// Single abstract permission.  Can add more friends or forwards.
class X::AttorneyY {
  friend void Y::usesX(int, X *, int);
  inline static void restricted(X &x, int n) { x.restricted(n); }
};

私はソフトウェア組織の第一人者ではありませんが、言語のこの側面では、インターフェイスのシンプルさと最小特権の原則が直接対立しているように感じます。私の希望のより明確な例は、、、またはインスタンス/メンバーメソッドのみがそれぞれ呼び出しを検討する必要があるおよびPersonのような宣言されたメソッドを持つクラスです。主要なインターフェイスの側面ごとに 1 回限りのプロキシ クラスまたはインターフェイス クラスが必要になるのは、私にはよくありません。takePill(Medicine *) tellTheTruth()forfeitDollars(unsigned int)PhysicianJudgeTaxMan

Drew Hallからの回答: Dr Dobbs - Friendship and the Attorney-Client Idiom

上記のコードは、もともと「弁護士」ではなく「プロキシ」というラッパー クラスを呼び出し、参照の代わりにポインターを使用していましたが、それ以外は Drew が見つけたものと同等でした。(あまり自分を責めないでください...) パラメータの転送を示すために、'restricted' の署名も変更しました。このイディオムの全体的なコストは、パーミッション セットごとに 1 つのクラスと 1 つのフレンド宣言、セットの承認された呼び出し元ごとに 1 つのフレンド宣言、およびパーミッション セットごとに公開されたメソッドごとに 1 つの転送ラッパーです。以下のより良い議論のほとんどは、非常によく似た「キー」イディオムが直接的な保護を犠牲にして回避する転送呼び出しのボイラープレートを中心に展開しています。

4

6 に答える 6

74

過去にさかのぼってPassKeyと呼ばれている非常に単純なパターンがあり、C++11 では非常に簡単です

template <typename T>
class Key { friend T; Key() {} Key(Key const&) {} };

そしてそれで:

class Foo;

class Bar { public: void special(int a, Key<Foo>); };

そして、呼び出しサイトは、どのFoo方法でも、次のようになります。

Bar().special(1, {});

注: C++03 で行き詰まっている場合は、記事の最後までスキップしてください。

コードは一見シンプルで、詳しく説明する価値のあるいくつかの重要なポイントが埋め込まれています。

パターンの要点は次のとおりです。

  • 呼び出しには、呼び出し元のコンテキストでBar::speciala をコピーする必要がありますKey<Foo>
  • のみFooを構築またはコピーできますKey<Foo>

次の点に注意してください。

  • 友情は推移的ではないため、から派生したクラスFooは構築またはコピーできませんKey<Foo>
  • Fooそれを呼び出すには、インスタンスを保持するだけでなく、コピーを作成する必要があるため、それ自体Key<Foo>を誰かに渡すことはできません。Bar::special

C++ は C++ であるため、避けるべき落とし穴がいくつかあります。

  • コピー コンストラクターはユーザー定義である必要があります。それ以外の場合はpublic、デフォルトです。
  • デフォルトのコンストラクターはユーザー定義でなければなりません。それ以外の場合はpublicデフォルトです
  • デフォルトのコンストラクターは手動で定義する必要があります。これ= defaultは、集計の初期化が手動のユーザー定義のデフォルト コンストラクターをバイパスできるようにするためです (したがって、任意の型がインスタンスを取得できるようになります)。

Keyこれは非常に微妙なので、記憶から再現しようとするのではなく、上記の定義を逐語的にコピー/貼り付けすることをお勧めします。


委任を許可するバリエーション:

class Bar { public: void special(int a, Key<Foo> const&); };

この亜種では、 のインスタンスを持っている人なら誰でも をKey<Foo>呼び出すことができるため、 を作成できるのはBar::specialだけですが、信頼できる副官に資格情報を広めることができます。FooKey<Foo>

このバリアントでは、不正な副官がキーを漏らさないようにするために、コピー コンストラクターを完全に削除することができます。これにより、キーの有効期間を特定のレキシカル スコープに結び付けることができます。


C++03 では?

アイデアは似ていますが、それは重要でfriend T;はないため、各ホルダーに対して新しいキー タイプを作成する必要があります。

class KeyFoo { friend class Foo; KeyFoo () {} KeyFoo (KeyFoo const&) {} };

class Bar { public: void special(int a, KeyFoo); };

パターンは十分に繰り返されるため、タイプミスを避けるためにマクロを作成する価値があります。

集計の初期化は問題ではあり= defaultませんが、構文も利用できません。


長年にわたってこの回答の改善に貢献してくれた人々に感謝します。

  • Luc Tourailleは、コピー コンストラクターを完全に無効にし、委任バリアントでのみ機能するコメントで私を指摘してくれましたclass KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} };(インスタンスの保存を防ぎます)。
  • K-ballo、C++11 がどのように状況を改善したかを指摘してくれましたfriend T;
于 2010-07-10T11:07:18.873 に答える
19

Attorney-Client イディオムは、あなたが探しているものかもしれません。メカニズムは、メンバー プロキシ クラスのソリューションとそれほど変わりませんが、この方法はより慣用的です。

于 2010-07-10T01:28:13.980 に答える
3

Jeff Aldger の著書「C++ for real programs」で説明されているパターンを使用できます。特別な名前はありませんが、「宝石とファセット」と呼ばれています。基本的な考え方は次のとおりです。すべてのロジックを含むメイン クラスの中で、そのロジックのサブ部分を実装するいくつかのインターフェイス (実際のインターフェイスではありません) を定義します。これらの各インターフェイス (本に関するファセット) は、メイン クラス (gemstone) のロジックの一部へのアクセスを提供します。また、各ファセットは宝石インスタンスへのポインタを保持します。

これはあなたにとって何を意味しますか?

  1. 宝石の代わりにあらゆるファセットを使用できます。
  2. ファセットのユーザーは、宝石の構造について知る必要はありません。PIMPL パターンを介して前方宣言および使用できるからです。
  3. 他のクラスは、宝石ではなくファセットを参照できます。これは、特定のクラスに限られた数のメソッドを公開する方法に関する質問への回答です。

お役に立てれば。必要に応じて、このパターンをより明確に説明するために、ここにコード サンプルを投稿できます。

編集:コードは次のとおりです。

class Foo1; // This is all the client knows about Foo1
class PFoo1 { 
private: 
 Foo1* foo; 
public: 
 PFoo1(); 
 PFoo1(const PFoo1& pf); 
 ~PFoo(); 
 PFoo1& operator=(const PFoo1& pf); 

 void DoSomething(); 
 void DoSomethingElse(); 
}; 
class Foo1 { 
friend class PFoo1; 
protected: 
 Foo1(); 
public: 
 void DoSomething(); 
 void DoSomethingElse(); 
}; 

PFoo1::PFoo1() : foo(new Foo1) 
{} 

PFoo1::PFoo(const PFoo1& pf) : foo(new Foo1(*(pf
{} 

PFoo1::~PFoo() 
{ 
 delete foo; 
} 

PFoo1& PFoo1::operator=(const PFoo1& pf) 
{ 
 if (this != &pf) { 
  delete foo; 
  foo = new Foo1(*(pf.foo)); 
 } 
 return *this; 
} 

void PFoo1::DoSomething() 
{ 
 foo->DoSomething(); 
} 

void PFoo1::DoSomethingElse() 
{ 
 foo->DoSomethingElse(); 
} 

Foo1::Foo1() 
{ 
} 

void Foo1::DoSomething() 
{ 
 cout << “Foo::DoSomething()” << endl; 
} 

void Foo1::DoSomethingElse() 
{ 
 cout << “Foo::DoSomethingElse()” << endl; 
} 

EDIT2 : クラス Foo1 はより複雑になる可能性があります。たとえば、別の 2 つのメソッドが含まれています。

void Foo1::DoAnotherThing() 
{ 
 cout << “Foo::DoAnotherThing()” << endl; 
} 

void Foo1::AndYetAnother() 
{ 
 cout << “Foo::AndYetAnother()” << endl; 
} 

そして、それらは経由でアクセスできますclass PFoo2

class PFoo2 { 
    private: 
     Foo1* foo; 
    public: 
     PFoo2(); 
     PFoo2(const PFoo1& pf); 
     ~PFoo(); 
     PFoo2& operator=(const PFoo2& pf); 

     void DoAnotherThing(); 
     void AndYetAnother(); 
    };
void PFoo1::DoAnotherThing() 
    { 
     foo->DoAnotherThing(); 
    } 

    void PFoo1::AndYetAnother() 
    { 
     foo->AndYetAnother(); 
    } 

これらのメソッドはPFoo1クラス内にないため、それを介してアクセスすることはできません。このようにして、 の動作Foo1を 2 つ (またはそれ以上) のファセット PFoo1 と PFoo2 に分割できます。これらのファセット クラスはさまざまな場所で使用でき、その呼び出し元は Foo1 の実装を認識すべきではありません。たぶんそれはあなたが本当に望んでいるものではないかもしれませんが、あなたが望むものはC++では不可能です。これは回避策ですが、冗長すぎるかもしれません...

于 2010-07-10T01:31:08.680 に答える
2

これは古い質問であることは知っていますが、問題は依然として関連しています。Attorney-Client イディオムのアイデアは気に入っていますが、プライベート (または保護された) アクセスが許可されたクライアント クラス用の透過的なインターフェイスが必要でした。

これに似たことがすでに行われていると思いますが、ざっと見ても何も見つかりませんでした。次のメソッド (C++11 以上) は、(オブジェクトごとではなく) クラスごとに機能し、「プライベート クラス」によって使用される CRTP 基本クラスを使用してパブリック ファンクターを公開します。明示的にアクセス権を与えられたクラスのみが、ファンクターの operator() を呼び出すことができます。これにより、保存された参照を介して、関連するプライベート メソッドが直接呼び出されます。

関数呼び出しのオーバーヘッドはなく、唯一のメモリ オーバーヘッドは、公開が必要なプライベート メソッドごとに 1 つの参照です。このシステムは非常に用途が広いです。プライベート クラスで仮想関数を呼び出す場合と同様に、任意の関数シグネチャと戻り値の型が許可されます。

私にとって、主な利点は構文の 1 つです。明らかに、プライベート クラスではファンクター オブジェクトのやや醜い宣言が必要ですが、これはクライアント クラスに対して完全に透過的です。元の質問から取った例を次に示します。

struct Doctor; struct Judge; struct TaxMan; struct TheState;
struct Medicine {} meds;

class Person : private GranularPrivacy<Person>
{
private:
    int32_t money_;
    void _takePill (Medicine *meds) {std::cout << "yum..."<<std::endl;}
    std::string _tellTruth () {return "will do";}
    int32_t _payDollars (uint32_t amount) {money_ -= amount; return money_;}

public:
    Person () : takePill (*this), tellTruth (*this), payDollars(*this) {}

    Signature <void, Medicine *>
        ::Function <&Person::_takePill>
            ::Allow <Doctor, TheState> takePill;

    Signature <std::string>
        ::Function <&Person::_tellTruth>
            ::Allow <Judge, TheState> tellTruth;

    Signature <int32_t, uint32_t>
        ::Function <&Person::_payDollars>
            ::Allow <TaxMan, TheState> payDollars;

};


struct Doctor
{
    Doctor (Person &patient)
    {
        patient.takePill(&meds);
//        std::cout << patient.tellTruth();     //Not allowed
    }
};

struct Judge
{
    Judge (Person &defendant)
    {
//        defendant.payDollars (20);            //Not allowed
        std::cout << defendant.tellTruth() <<std::endl;
    }
};

struct TheState
{
    TheState (Person &citizen)                  //Can access everything!
    {
        citizen.takePill(&meds);
        std::cout << citizen.tellTruth()<<std::endl;
        citizen.payDollars(50000);
    };
};

GranularPrivacy 基本クラスは、ネストされた 3 つのテンプレート クラスを定義することによって機能します。これらの最初の 'Signature' は、関数の戻り値の型と関数のシグネチャをテンプレート パラメーターとして取り、これらをファンクターの operator() メソッドと 2 番目のネスト テンプレート クラス 'Function' の両方に転送します。これは、Host クラスのプライベート メンバー関数へのポインターによってパラメーター化されます。これには、Signature クラスによって提供される署名が必要です。実際には、2 つの別個の「関数」クラスが使用されます。ここで与えられたものと const 関数のための別のものは、簡潔にするために省略されています。

最後に、Allow クラスは、テンプレート引数リストで指定されたクラスの数に応じて、可変個引数テンプレート メカニズムを使用して、明示的にインスタンス化された基本クラスから再帰的に継承します。Allow の各継承レベルには、テンプレート リストから 1 つのフレンドがあり、using ステートメントは、基本クラスのコンストラクターと演算子 () を継承階層の最も派生したスコープに持ち込みます。

template <class Host> class GranularPrivacy        
{
    friend Host;
    template <typename ReturnType, typename ...Args> class Signature
    {
        friend Host;
        typedef ReturnType (Host::*FunctionPtr) (Args... args);
        template <FunctionPtr function> class Function
        {
            friend Host;
            template <class ...Friends> class Allow
            {
                Host &host_;
            protected:
                Allow (Host &host) : host_ (host) {}
                ReturnType operator () (Args... args) {return (host_.*function)(args...);}
            };
            template <class Friend, class ...Friends>
            class Allow <Friend, Friends...> : public Allow <Friends...>
            {
                friend Friend;
                friend Host;
            protected:
                using Allow <Friends...>::Allow;
                using Allow <Friends...>::operator ();
            };
        };
    };
};

コメントや提案は大歓迎です。これは間違いなくまだ進行中の作業です。特に、Signature クラスと Function クラスを 1 つのテンプレート クラスにマージしたいと考えていますが、これを行う方法を見つけるのに苦労しています。より完全で実行可能な例は、cpp.sh/6ev45およびcpp.sh/2rtrjにあります。

于 2015-11-21T03:15:14.327 に答える
0

friend以下のコードに似たものを使用すると、プライベートな状態のどの部分をキーワードで公開するかを細かく制御できます。

class X {
  class SomewhatPrivate {
    friend class YProxy1;

    void restricted();
  };

public:
  ...

  SomewhatPrivate &get_somewhat_private_parts() {
    return priv_;
  }

private:
  int n_;
  SomewhatPrivate priv_;
};

しかし:

  1. 努力する価値はないと思います。
  2. キーワードを使用する必要があるというfriendことは、設計に欠陥があることを示唆している可能性があります。おそらく、キーワードを使用せずに必要なことを実行できる方法があります。私はそれを避けようとしますが、それによってコードが読みやすくなり、保守しやすくなり、ボイラープレート コードの必要性が減る場合は、それを使用します。

編集:私にとって、上記のコードは(通常)(通常)使用すべきではない忌まわしきものです

于 2010-07-10T01:19:13.673 に答える