54

为什么 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 Hall接受的答案:Dr Dobbs - Friendship and the Attorney-Client Idiom

上面的代码最初将包装类称为“代理”而不是“律师”,并使用指针而不是引用,但在其他方面与 Drew 发现的相同,然后我认为这是最广为人知的解决方案。(不要太用力地拍自己的背……)我还更改了“restricted”的签名来演示参数转发。这个习惯用法的总成本是每个权限集一个类和一个朋友声明,每个批准的调用者一个朋友声明,每个权限集每个公开的方法一个转发包装器。下面大多数更好的讨论都围绕着转发呼叫样板,一个非常相似的“关键”习语以较少的直接保护为代价避免了这种样板。

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::special复制 aKey<Foo>
  • 只能Foo构造或复制一个Key<Foo>

值得注意的是:

  • 派生自的类Foo不能构造或复制Key<Foo>,因为友谊不具有传递性
  • Foo它本身不能传递 aKey<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;
于 2010-07-10T11:07:18.873 回答
19

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

于 2010-07-10T01:28:13.980 回答
3

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

这对你意味着什么?

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

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

编辑:这是代码:

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。这些 facets 类可以在不同的地方使用,它们的调用者不应该知道 Foo1 的实现。也许这不是你真正想要的,但你想要的对于 C++ 来说是不可能的,这是一种解决方法,但可能过于冗长......

于 2010-07-10T01:31:08.680 回答
2

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

我想类似的事情已经完成了,但是粗略地环顾四周并没有发现任何东西。以下方法(C++11 以上)在每个类(而不是每个对象)的基础上工作,并使用“私有类”使用的 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',将函数返回类型和函数签名作为模板参数,并将它们转发给仿函数的 operator() 方法和第二个嵌套模板类,'Function'。这由指向 Host 类的私有成员函数的指针参数化,该函数必须具有 Signature 类提供的签名。在实践中,使用了两个单独的“功能”类;这里给出的一个,另一个用于 const 函数,为简洁起见省略。

最后,Allow 类使用可变参数模板机制递归地从显式实例化的基类继承,具体取决于它的模板参数列表中指定的类的数量。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找到更完整、可运行的示例。

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

于 2017-05-12T03:01:08.277 回答