Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
395 views
in Technique[技术] by (71.8m points)

design patterns - clean C++ granular friend equivalent? (Answer: Attorney-Client Idiom)

Why does C++ have public members that anyone can call and friend declarations that expose all private members to given foreign classes or methods but offer no syntax to expose particular members to given callers?

I want to express interfaces with some routines to be invoked only by known callers without having to give those callers complete access to all privates, which feels like a reasonable thing to want. The best I could come up with myself (below) and suggestions by others so far revolve around idioms/pattern of varying indirectness, where I really just want a way to have single, simple class definitions that explicitly indicate what callers (more granularly than me, my children, or absolutely anybody) can access which members. What is the best way to express the concept below?

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

I'm nowhere near being a software organization guru, but it feels like interface simplicity and the principle of least privilege are directly at odds in this aspect of the language. A clearer example for my desire might be a Person class with declared methods like takePill(Medicine *) tellTheTruth() and forfeitDollars(unsigned int) that only Physician, Judge, or TaxMan instances/member methods, respectively, should even consider invoking. Needing one-time proxy or interface classes for each major interface aspect sits ill with me, but please speak up if you know I'm missing something.

Answer accepted from Drew Hall: Dr Dobbs - Friendship and the Attorney-Client Idiom

The code above originally called the wrapper class 'Proxy' instead of 'Attorney' and used pointers instead of references but was otherwise equivalent to what Drew found, which I then deemed the best generally known solution. (Not to pat myself on the back too hard...) I also changed the signature of 'restricted' to demonstrate parameter forwarding. The overall cost of this idiom is one class and one friend declaration per permission set, one friend declaration per set approved caller, and one forwarding wrapper per exposed method per permission set. Most of the better discussion below revolves around the forwarding call boilerplate that a very similar 'Key' idiom avoids at the expense of less direct protection.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

There is a very simple pattern, which has retro-actively been dubbed PassKey, and which is very easy in C++11:

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

And with that:

class Foo;

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

And the call site, in any Foo method, looks like:

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

Note: if you are stuck in C++03, skip to the end of the post.

The code is deceptively simple, it embeds a few key points that are worth elaborating.

The crux of the pattern is that:

  • calling Bar::special requires copying a Key<Foo> in the context of the caller
  • only Foo can construct or copy a Key<Foo>

It is notable that:

  • classes derived from Foo cannot construct or copy Key<Foo> because friendship is not transitive
  • Foo itself cannot hand down a Key<Foo> for anyone to call Bar::special because calling it requires not just holding on to an instance, but making a copy

Because C++ is C++, there are a few gotchas to avoid:

  • the copy constructor has to be user-defined, otherwise it is public by default
  • the default constructor has to be user-defined, otherwise it is public by default
  • the default constructor has to be manually defined, because = default would allow aggregate initialization to bypass the manual user-defined default constructor (and thus allow any type to get an instance)

This is subtle enough that, for once, I advise you to copy/paste the above definition of Key verbatim rather than attempting to reproduce it from memory.


A variation allowing delegation:

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

In this variant, anyone having an instance of Key<Foo> can call Bar::special, so even though only Foo can create a Key<Foo>, it can then disseminate the credentials to trusted lieutenants.

In this variant, to avoid a rogue lieutenant leaking the key, it is possible to delete the copy constructor entirely, which allows tying the key lifetime to a particular lexical scope.


And in C++03?

Well, the idea is similar, except that friend T; is not a thing, so one has to create a new key type for each holder:

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

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

The pattern is repetitive enough that it might be worth a macro to avoid typos.

Aggregate initialization is not an issue, but then again the = default syntax is not available either.


Special thanks to people who helped improving this answer over the years:

  • Luc Touraille, for pointing to me in the comments that class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; completely disables the copy constructor and thus only works in the delegation variant (preventing storing instance).
  • K-ballo, for pointing out how C++11 improved the situation with friend T;

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...