C++中的clean granular friend等效是什么?(答案:律师-客户端语法)

60
为什么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”的签名以演示参数转发。这种惯用法的总成本是每个权限集合一个类和一个友元声明,每个已批准调用者一个友元声明,每个暴露方法每个权限集合一个转发包装器。大部分更好的讨论都围绕着非常相似的“Key”惯用法避免了转发调用样板代码,但以更少直接的保护为代价。


我认为这是一个有趣的语言设计问题 - 你的理想语言会如何处理这个问题?但是,对于只需要很少次使用好友功能来说,这似乎过于复杂了。如果需要更细粒度的访问控制,通常注释或函数名前缀就足够了。我更喜欢 Python 的方法 - 即“用户应该做正确的事情”,而不是试图强制执行它。 - Stephen
1
@Stephen:如果语言集成是可能的,我更多地考虑了 AccessGroup foo { class A; void f(); };void g() restrictedTo(foo); 这样的方式。哦,好吧,人总是要做梦的... :) - Georg Fritzsche
@Georg:好主意,我也在考虑一种装饰器的方法,类似于Python或Java的装饰器(比如@Access(Foo))。也许这个问题在C++2x中得到了解决 :) - Stephen
1
@Stephen:我更倾向于Georg的观点,即鼓励正确使用模块的代码。如果信任是真正的保障,那么私有访问甚至不需要存在,我更喜欢让代码本身执行策略。很多狂热者认为所有注释都是邪恶的,最多只能是准确的,即使如此也只有在完美的情况下才能做到。我确实喜欢你提出的集成语法,但我也希望有更简洁的选项,比如void g() restrictedTo<int B::f(), class C>;。我还没有真正考虑过语法,但完整的实体可能并不总是必要的。 - Jeff
您更简洁的版本应该基本上可以使用属性作为扩展来实现。也许有人会花费太多时间来推动它进入Clang?;)请注意,概念只是被推迟了,否则它们将会更加延迟新的标准,请参见这里 - Georg Fritzsche
显示剩余4条评论
6个回答

90

有一个非常简单的模式,被称为PassKey,在C++11中非常容易实现: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`可以构造或复制`Key`。
值得注意的是:
- 派生自`Foo`的类无法构造或复制`Key`,因为友元关系不具有传递性。 - `Foo`本身无法传递一个`Key`给任何人调用`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;来改善情况。

1
我不禁要想,为什么你要通过引用传递FooKey。;) 对于一个空的、未使用的对象来说,这似乎是一件奇怪的事情。除此之外,我喜欢这个想法,而且我以前也曾经按需求做过同样的事情,但这是我第一次真正将其考虑为一般模式。 +1 - jalf
1
我在 Stack Overflow 上发布了一个关于这种模式命名的问题。 - Georg Fritzsche
1
@Jeff:我认为每个体面的编译器都应该在按值传递时优化空类,除非你开始获取其地址或类似操作。但我同意我有些低估了其侵入性。 - Georg Fritzsche
1
你确定你使用 boost::noncopyable 的解决方案能够正确工作吗?为了按值传递键,Foo 需要访问 FooKey 的复制构造函数,但是由于需要访问 boost::noncopyable 的复制构造函数,所以无法编译。我认为为了使其正常工作,你需要放弃 noncopyable 并将复制构造函数设为私有(毕竟,该类并非真正的不可复制,只是复制被限制在友元中)。 - Luc Touraille
2
不要忘记这个很棒的调用语法:Bar b; b.special( 3, {} ); 我们也可以使用 static struct unlock_request_t {} use_key; template<class T> class only_allow { friend class T; only_allow(unlock) {}; only_allow(only_allow const&)=delete; },这将更改访问模式为 Bar b; b.special( 3, use_key ); ;) - Yakk - Adam Nevraumont
显示剩余13条评论

21

律师客户(Attorney-Client idiom)可能是你正在寻找的。其机制与会员代理类解决方案并没有太大区别,但这种方式更加惯用。


我只知道它的名称,但迄今为止我所看到的看起来非常有前途。这可能是我正在寻找的东西。谢谢! - Jeff
大家好,这几乎是我重新发明的内容,我认为这个链接值得一读。不同之处在于,在发布的形式中,辅助类被称为XAttourney而不是X :: Proxy2,并且包装调用使用引用而不是指针。我将分别使用称为X :: YAttourney的成员,这些成员在引用上使用静态调用。由于这是关于“如何”的研究和规范答案,因此我接受了这个答案,并将把“为什么友元本身是全有或全无”拆分成一个新问题。 - Jeff

3

我知道这是一个老问题,但问题仍然存在。虽然我喜欢律师-客户惯用语的想法,但我希望客户类有一个透明的接口,可以获得private(或protected)访问权限。

我认为类似的事情已经做过了,但粗略的查看并没有找到任何东西。下面的方法(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 基类通过定义三个嵌套的模板类来工作。第一个类'Signature',将函数返回类型和函数签名作为模板参数,并将它们转发到函数对象的operator()方法和第二个嵌套模板类 'Function'中。这个类由指向Host类的私有成员函数的指针参数化,该函数必须具有Signature类提供的签名。在实践中,使用了两个单独的'Function'类; 这里是其中一个,另一个用于const函数,为了简洁起见省略了。
最后,Allow类通过使用可变模板机制显式实例化的基类递归地继承,取决于其模板参数列表中指定的类数。每个Allow的继承级别具有来自模板列表的一个友元,并且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类合并为一个模板类,但一直在努力寻找方法。更完整、可运行的示例可以在cpp.sh/6ev45cpp.sh/2rtrj找到。


我喜欢这种方法,因为它保留了函数调用,不像keypass或attorney-client。此外,我想可以使用static_assert等来给出一些合理的编译错误。然而,在我的环境中,VS2017 C++17上无法编译通过。后面的链接(cpp.sh/2rtrj)会输出一堆编译错误。经过几个小改动(见cpp.sh/9d55s),我得到了可读的代码: - llf
"'GranularPrivacy<Person>::Signature<void,Medicine *>::Function<void Person::_takePill(Medicine *)>::Allow<Doctor,TheState>::operator ()': 无法访问类'GranularPrivacy<Person>::Signature<void,Medicine *>::Function<void Person::_takePill(Medicine *)>::Allow<Doctor,TheState>'中声明的受保护成员。" 实际上,所有针对 TheState 的调用都失败了,VS 告诉我 "function GranularPrivacy<Host>:: ... ::Allow<Friends>::operator() [with Host=Person, ...., Friends=<>]" 是不可访问的。您对这些错误有什么见解吗? - llf
很遗憾,我们没有可变参数的朋友。 - Jarod42

3
你可以使用Jeff Aldger的 'C++ for real programmers'中描述的模式。它没有特殊的名称,但在书中被称为“gemstones和facets”。基本思想如下:在包含所有逻辑的主类中,定义几个实现该逻辑子部分的接口(不是真正的接口,只像它们)。每个这些接口(在书中术语为facet)都提供对主类(gemstone)某些逻辑的访问。此外,每个facet都保存指向gemstone实例的指针。
这对您意味着什么?
  1. 您可以在任何地方使用任何facet代替gemstone。
  2. facet的用户不必了解gemstone结构,因为它可以通过PIMPL模式进行预声明并使用。
  3. 其他类可以引用facet而不是gemstone-这是回答您关于如何向指定类公开有限数量方法的问题的答案。
希望这能帮到你。如果您愿意,我可以在这里发布代码示例,以更清楚地说明此模式。 编辑: 这是代码:
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的行为分成两个(或更多)方面:PFoo1和PFoo2。这些方面的类可以在不同的地方使用,它们的调用者不应该知道Foo1的实现。也许这不是您真正想要的,但对于C++来说,这是一种解决方法,但可能太冗长了...

1
谢谢您的帖子,但我不确定我清楚这个模式的目的。它似乎是基于vanilla pImpl风格的实现隐藏,而我只想要一个扁平但有选择性的接口描述。您能详细解释一下它的细微差别吗? - Jeff

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

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


使用友元并不意味着设计有缺陷。不适当的友元使用才会导致问题,但从这个问题的上下文中我们无法推断出这种情况。人们害怕使用友元,因为他们声称这违反了封装性。然而,当正确使用时,友元实际上有益于封装性。[参考 Stroustrup] - Shirik
Shirik: 因此,“可能建议” - Staffan
语句“可能表明”完全基于谓词“使用友谊存在”。这种相关性不应存在。 - Shirik
我所追求的并不是数据隐藏,而是授予可信接口高级例程访问权限。这里绝对没有必要使用friend,但我强烈希望能够尽可能地控制外部访问的范围。 - Jeff

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类,因为它仍然是平凡的。然而,效果应该是相同的,因为没有人会存储这些对象。


这个解决方案是否适用于以下问题:https://stackoverflow.com/questions/53172759/templated-attorney-client-idiom-for-more-many-classes - Blood-HaZaRd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接