干净的C ++粒度朋友相当于? (答案:律师 - 客户成语)

问题描述 投票:45回答:6

为什么C ++有public成员,任何人都可以调用和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类,声明的方法,如takePill(Medicine *) tellTheTruth()forfeitDollars(unsigned int),只有PhysicianJudgeTaxMan实例/成员方法,分别应该考虑调用。每个主要界面方面需要一次性代理或接口类对我不好,但如果你知道我错过了什么,请说出来。

答案来自Drew HallDr Dobbs - Friendship and the Attorney-Client Idiom

上面的代码最初称为包装类'Proxy'而不是'Attorney',并使用指针代替引用,但在其他方面等同于Drew发现的,我认为这是最常见的解决方案。 (不要太苛刻地背拍自己......)我还改变了'restricted'的签名来演示参数转发。这个习语的总成本是每个权限集一个类和一个朋友声明,每个集合批准的一个朋友声明,每个权限集每个暴露方法一个转发包装器。以下大多数更好的讨论围绕着转发呼叫样板,一个非常相似的“密钥”习语避免以较少的直接保护为代价。

c++ design-patterns private friend
6个回答
17
投票

Attorney-Client idiom可能是您正在寻找的。机制与您的成员代理类解决方案没有太大区别,但这种方式更具惯用性。


62
投票

有一个非常简单的模式,后来被称为PassKey,这是very easy in 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::special需要在调用者的上下文中复制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,所以即使只有Foo可以创建Key<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;的情况

2
投票

我知道这是一个老问题,但问题仍然存在。虽然我喜欢Attorney-Client习惯用法的想法,但我希望为已被授予私有(或受保护)访问权限的客户端类提供透明接口。

我想象已经完成了与此类似的事情,但粗略地看一下并没有发现任何事情。以下方法(C ++ 11 up)基于每个类(不是每个对象)工作,并使用“私有类”使用的CRTP基类来公开公共函子。只有那些具有特定访问权限的类才能调用仿函数的operator(),然后通过存储的引用直接调用相关的私有方法。

没有函数调用开销,唯一的内存开销是每个需要曝光的私有方法的一个引用。该系统用途广泛;允许任何函数签名和返回类型,就像调用私有类中的虚函数一样。

对我来说,主要的好处是语法之一。虽然在私有类中需要一个公认的相当丑陋的仿函数对象声明,但这对客户端类是完全透明的。以下是原始问题的示例:

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',将函数返回类型和函数签名作为模板参数,并将它们转发给functor的operator()方法和第二个nest模板类'Function'。这是通过指向Host类的私有成员函数的参数来进行的,该函数必须具有Signature类提供的签名。在实践中,使用了两个单独的“函数”类;这里给出的是另一个用于const函数的,为简洁起见省略了。

最后,Allow类使用variadic模板机制递归地从显式实例化的基类继承,具体取决于在其模板参数列表中指定的类的数量。 Allow的每个继承级别都有一个来自模板列表的朋友,而using语句将继承层次结构的基类构造函数和operator()带入最派生的范围。

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类合并到一个模板类中,但一直在努力寻找一种方法来实现这一点。更多完整,可运行的例子可以在cpp.sh/6ev45cpp.sh/2rtrj找到。


2
投票

您可以使用Jeff Aldger的book'C ++中为真正的程序员'描述的模式。它没有特别的名称,但它被称为“宝石和刻面”。基本思想如下:在包含所有逻辑的主类中,您定义了几个实现该逻辑子部分的接口(不是真正的接口,就像它们一样)。这些接口中的每一个(就书的方面而言)提供了对主类(gemstone)的一些逻辑的访问。此外,每个facet都包含指向gemstone实例的指针。

这对你意味着什么?

  1. 你可以在任何地方使用任何方面而不是宝石。
  2. facet的用户不必了解gemstone结构,因为可以通过PIMPL模式进行前向声明和使用。
  3. 其他类可以引用facet而不是gemstone - 这是关于如何将有限的nubmer方法暴露给指定类的问题的答案。

希望这可以帮助。如果你愿意,我可以在这里发布代码示例,以更清楚地说明这种模式。

编辑:这是代码:

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可能更复杂,例如,它包含另外两种方法:

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的行为拆分为两个(或更多)方面PFoo1和PFoo2。那些facet类可以在不同的地方使用,并且他们的调用者不会意识到Foo1的实现。也许这不是你真正想要的,但是你想要的东西对于C ++来说是不可能的,这是一个令人工作的工作,但可能过于冗长......


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关键字的需要可能表明您的设计存在缺陷,也许有一种方法可以在没有它的情况下完成您的需求。我试图避免它,但如果它使代码更易读,可维护或减少对样板代码的需要我使用它。

编辑:对我来说,上面的代码(通常)是一个应该(通常)不被使用的憎恶。


0
投票

我对Matthieu M.给出的解决方案进行了一些小的改进。他的解决方案的局限在于你只能授予对单个类的访问权限。如果我想让三个班级中的任何一个有权访问该怎么办?

#include <type_traits>
#include <utility>

struct force_non_aggregate {};

template<typename... Ts>
struct restrict_access_to : private force_non_aggregate {
    template<typename T, typename = typename std::enable_if<(... or std::is_same<std::decay_t<T>, std::decay_t<Ts>>{})>::type>
    constexpr restrict_access_to(restrict_access_to<T>) noexcept {}
    restrict_access_to() = delete;
    restrict_access_to(restrict_access_to const &) = delete;
    restrict_access_to(restrict_access_to &&) = delete;
};

template<typename T>
struct access_requester;

template<typename T>
struct restrict_access_to<T> : private force_non_aggregate {
private:
    friend T;
    friend access_requester<T>;

    restrict_access_to() = default;
    restrict_access_to(restrict_access_to const &) = default;
    restrict_access_to(restrict_access_to &&) = default;
};

// This intermediate class gives us nice names for both sides of the access
template<typename T>
struct access_requester {
    static constexpr auto request_access_as = restrict_access_to<T>{};
};


template<typename T>
constexpr auto const & request_access_as = access_requester<T>::request_access_as;

struct S;
struct T;

auto f(restrict_access_to<S, T>) {}
auto g(restrict_access_to<S> x) {
    static_cast<void>(x);
    // f(x); // Does not compile
}

struct S {
    S() {
        g(request_access_as<S>);
        g({});
        f(request_access_as<S>);
        // f(request_access_as<T>); // Does not compile
        // f({request_access_as<T>});   // Does not compile
    }
};

struct T {
    T() {
        f({request_access_as<T>});
        // g({request_access_as<T>}); // Does not compile
        // g({}); // Does not compile
    }
};

这使用稍微不同的方法来使对象不是聚合。我们有一个空的私有基类,而不是用户提供的构造函数。在实践中,它可能无关紧要,但这意味着此实现是一个POD类,因为它仍然是微不足道的。然而,效果应该保持不变,因为无论如何都没有人会存储这些物体。

© www.soinside.com 2019 - 2024. All rights reserved.