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

为什么C ++有任何人都可以调用的public成员和friend声明, 这些声明将所有 private成员公开给定的外部类或方法,但是没有提供给给定调用者公开特定成员的语法?

我想用一些例程来表示接口,只能由已知的调用者调用,而不必让这些调用者完全访问所有的私有,感觉这是一个合理的想法。 最好的我可以自己(下)和其他人的build议到目前为止围绕不同的间接性的习语/模式,我真的只想要一个单一的 ,简单的类定义,明确指出什么来电者(比更细) , 我的孩子 ,或绝对任何人 )可以访问哪些成员。 以下expression概念的最佳方式是什么?

 // 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 Hall接受的答复: Dobbs博士 – 友谊和律师 – 客户成语

上面的代码最初称为包装类“代理”,而不是“律师”,并使用指针而不是引用,但在其他方面相当于德鲁发现,然后我认为是最好的公认的解决scheme。 (不要太费力地背对自己)我也改变了“限制”的签名来演示参数转发。 这个习惯用法的总成本是每个权限集合一个类别和一个朋友声明,每个集合批准的调用者一个朋友声明,以及每个权限集合每个暴露方法的一个转发包装。 下面大多数更好的讨论围绕转发呼叫样板,一个非常相似的“关键”成语避免了在较less的直接保护的代价。

律师客户的成语可能是你要找的。 这个机制与你的成员代理类解决scheme没有太大的区别,但是这种方式更具有惯用性。

有一个非常简单的模式,它被复古的被称为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中,跳到post的末尾。

代码看起来很简单,它embedded了一些值得阐述的关键点。

关键在于:

  • 调用Bar::special需要在调用者的上下文中复制Key<Foo>
  • 只有Foo可以构造或复制一个Key<Foo>

值得注意的是:

  • 派生自Foo类不能构造或复制Key<Foo>因为友谊不是传递的
  • Foo本身不能传递一个Key<Foo>给任何人调用Bar::special因为调用它需要的不仅仅是一个实例,而是一个副本

因为C ++是C ++,所以有一些问题需要避免:

  • 复制构造函数必须是用户定义的,否则默认情况下是public
  • 默认的构造函数必须是用户自定义的,否则默认为public
  • 默认的构造函数必须手动定义,因为= default将允许聚合初始化绕过手动的用户定义的默认构造函数(并因此允许任何types获取实例)

这很微妙,我build议你一次性复制/粘贴Key的上述定义,而不是试图从内存中复制它。


允许授权的变体:

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

在这个变种中,任何拥有Key<Foo>实例的人都可以调用Bar::special ,所以即使只有Foo可以创build一个Key<Foo> ,它才可以将信任传递给信任的中尉。

在这个变体中,为了避免stream氓中尉泄漏密钥,可以完全删除复制构造函数,这可以将密钥生存期绑定到特定的词法范围。


而在C + + 03?

那么,这个想法是相似的,除了friend T; 是不是一个东西,所以必须为每个持有人创build一个新的密钥types:

 class KeyFoo { friend class Foo; KeyFoo () {} KeyFoo (KeyFoo const&) {} }; class Bar { public: void special(int a, KeyFoo); }; 

该模式是重复的,可能是值得一个macros,以避免错别字。

聚集初始化不是一个问题,但是再次= default语法也不可用。


特别感谢这些年来帮助改善这个问题的人:

  • Luc Touraille ,指着我在class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} };中的注释class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; 完全禁用复制构造函数,因此只能在代理变体中使用(防止存储实例)。
  • K-ballo ,指出C ++ 11如何改善与friend T;的情况friend T;

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

这对你意味着什么?

  1. 你可以使用任何方面而不是gem。
  2. 小平面的使用者不必知道gem结构,因为可以通过PIMPL模式预先声明和使用gem结构。
  3. 其他类可以引用方面,而不是gem – 这是你的问题的答案如何暴露有限的方法指定类的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; } 

编辑2:您的类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的行为Foo1两个(或更多)方面PFoo1和PFoo2。 这些方面的类可以用在不同的地方,他们的调用者不应该知道Foo1的实现。 也许这不是你真正想要的,但是你想要的对于C ++来说是不可能的,这是一个工作上的问题,但也许太冗长了…

我知道这是一个老问题,但问题仍然是相关的。 虽然我喜欢“律师客户”这一习语,但我想要一个透明的客户端类的接口,它已被授予私有(或受保护的)访问权限。

我想象一下类似的东西已经做了,但粗略的环顾四周没有任何东西。 以下方法(C ++ 11 up)在每个类(而不是每个对象)的基础上工作,并使用“私有类”使用的CRTP基类来公开一个公共函子。 只有那些具有特定访问权限的类才能调用函子的operator(),然后通过存储的引用直接调用关联的私有方法。

没有函数调用开销,唯一的内存开销是每个需要暴露的私有方法的引用。 该系统非常灵活, 允许任何函数签名和返回types,就像调用私有类中的虚函数一样。

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

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

最后,Allow类使用可变参数模板机制从显式实例化的基类中recursion地inheritance,这取决于在其模板参数列表中指定的类的数量。 Allow的每个inheritance级别都有一个来自模板列表的好友,而using语句则将inheritance层次结构中的基类构造函数和操作符()带入最派生的范围。

 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 (); }; }; }; }; 

我希望有人认为这是有用的,任何意见或build议将是最受欢迎的。 这肯定还在进行中 – 我特别想将Signature和Function类合并成一个模板类,但一直在努力寻找一种方法来做到这一点。 更完整的可运行示例可以在cpp.sh/6ev45和cpp.sh/2rtrjfind。

类似于下面的代码将允许您通过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关键字的需要可能意味着你的devise是有缺陷的,也许有一种方法可以在没有它的情况下做你所需要的。 我试图避免它,但如果它使代码更具可读性,可维护性或减less对样板代码的需求,我使用它。

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

我已经对Matthieu M提出的解决scheme做了一些小小的改进。他的解决scheme的局限性是您只能授予一个class级的访问权限。 如果我想让三个类中的任何一个有权访问呢?

 #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类,因为它仍然是微不足道的。 但效果应该保持不变,因为无论如何都不会有人存储这些对象。