Picky Friendship: Access Control in C++
Sander Stoks
When your friend wants to borrow one of your books, you'd probably give it without second thoughts – that's what friends are for. When it comes to your car, you'd probably be a little more picky. And you may want to keep your toothbrush strictly for yourself.
In C++, there's no such distinction. A friend
is a friend
, and
everything you consider private
is shared equally among them. "Mi clasa es tu clasa".
In this article, I present a way to implement selective access to the interfaces a class implements.
Sometimes you want to allow certain classes access to specific interfaces, but declaring those classes
friend
means they get access to all your member functions - and even to your private member data.
You could declare those specific member functions protected
, limiting access to classes
inheriting from yours, but there is no way to limit this inheritance to only certain "heirs".
The limitation of the current technique is that the granularity is on the interface level, so you would have to put the "precious" member functions in their own interfaces. Although a little far-fetched, let's stick with the example of the first paragraph, and define the interfaces
struct BookLender { virtual void BorrowBook() = 0; }; struct CarLender { virtual void BorrowCar() = 0; };
And further suppose you have a class representing a person, implementing these interfaces:
class Person : public BookLender, public CarLender { public: // anyone can borrow some sugar void BorrowSugar() { cout << "Here's some sugar" << endl; } private: void BorrowBook() { cout << "Here's a book" << endl; } void BorrowCar() { cout << Here's my car" << endl; } Toothbrush m_Toothbrush; // nobody touches my toothbrush! };
First, let's make sure we don't allow access to the BookLender
and CarLender
interfaces by accident. Although they are private member functions, anyone could simply do,
Person p; CarLender* c = &p; c->BorrowCar();
You could make it derive protected instead, and also do the "sealed" trick:
class Sealed { Sealed(); friend class Person; }; class Person : virtual Sealed, protected BookLender, protected CarLender { public: void BorrowSugar() { } protected: void BorrowBook() { } void BorrowCar() { } private: Toothbrush m_Toothbrush; };
This way, nobody can inherit from Person
(and consequently, access the protected
member functions) unless they are listed as friends in the Sealed
class. This is
better than making these "trusted" classes outright friend
s of the Person
class since those "selected heirs" can not access your private
toothbrush, but there
is still no discrimination between friends who can borrow a book and friends who can borrow your car.
Besides, we should forget about this whole inheritance thing as a mechanism to control your client
code, as it is not the correct paradigm for this.
Let's derive from the interfaces privately instead:
class Person : private BookLender, private CarLender { public: void BorrowSugar() { } private: void BorrowBook() { } void BorrowCar() { } Toothbrush m_Toothbrush; };
This cuts short any attempts to access the private interfaces directly, since
Person p; CarLender* c = &p; c->BorrowCar();
will give a compiler error like "conversion from Person*
to CarLender*
exists, but is inaccessible". So far, so good.
Now suppose there's a class Colleague
, which you'd like to allow borrowing your books,
but not your car. Declaring Colleague
a friend
class is not an option,
since then any Colleague
not only can borrow your car, but has full access to your toothbrush
– yuck. Even a class GoodFriend
is not allowed access to your toothbrush, but
borrowing the car is fine. In code, we want something like this:
class Colleague { public: Colleague(Person& p) : m_p(p) {} void CallPrivateFunctions() { m_p.BorrowBook(); // allowed m_p.BorrowCar(); // not allowed use(m_p.m_Toothbrush); // not allowed } private: Person& m_p; }; class GoodFriend { public: GoodFriend(Person& p) : m_p(p) {} void CallPrivateFunctions() { m_p.BorrowBook(); // allowed m_p.BorrowCar(); // allowed use(m_p.m_Toothbrush); // not allowed } private: Person& m_p; };
But how can we get this to work, with as little extra code as possible, and preferably with an intuitive syntax? The trick lies in a little helper template class:
template <class C, class Itf> class Access { protected: Access(C& c) : m_class(c) {} operator Itf*() { return &m_class; } C& m_class; };
If you now extend your Person
class a little bit:
class Person : private BookLender, private CarLender { public: void BorrowSugar() { } template <class C, class Itf> friend class Access; class BookAccess : public Access{ BookAccess(Person& p) : Access (p) {} friend class Colleague; friend class GoodFriend; }; class CarAccess : public Access { CarAccess(Person& p) : Access (p) {} friend class GoodFriend; }; private: void BorrowBook() { } void BorrowCar() { } Toothbrush m_Toothbrush; };
the only thing you need to add to the friend
classes is the following:
class Colleague { public: Colleague(Person& p) : m_p(p) {} void CallPrivateFunctions() { BookLender* b = Person::BookAccess(m_p); // allowed b->BorrowBook(); // allowed CarLender* c = Person::CarAccess(m_p); // not allowed c->BorrowCar(); // not allowed use(m_p.m_Toothbrush); // not allowed } private: Person& m_p; }; class GoodFriend { public: GoodFriend(Person& p) : m_p(p) {} void CallPrivateFunctions() { BookLender* b = Person::BookAccess(m_p); // allowed b->BorrowBook(); // allowed CarLender* c = Person::CarAccess(m_p); // allowed c->BorrowCar(); // allowed use(m_p.m_Toothbrush); // not allowed } private: Person& m_p; };
The idea is that via the Access
class, you allow others (but only friend
s!)
to use a conversion operator which yields access to the "raw" interfaces. You define such a class
once per interface you want to share, and list anyone as friend
in that class definition
whom you like to grant access to this particular interface.
Finally, if you grew up in the "MFC/COM/ATL camp", you will perhaps appreciate some preprocessor help to make things more readable:
#define ENABLE_ACCESS templatefriend class Access #define GRANT_ACCESS(C, itf) \ class itf##Access : public Access<C, itf> \ { \ itf##Access(C& c) : Access<C, itf>(c) {} #define END_GRANT_ACCESS } #define BY friend class
using these macros, the code for Person
becomes:
class Person : private BookLender, private CarLender { public: void BorrowSugar() { } ENABLE_ACCESS; GRANT_ACCESS(Person, BookLender); BY Colleague; BY GoodFriend; END_GRANT_ACCESS; GRANT_ACCESS(Person, CarLender); BY GoodFriend; END_GRANT_ACCESS; private: void BorrowBook() { } void BorrowCar() { } Toothbrush m_Toothbrush; };
By the way: my introductionary computer programming book is available here. It uses C and stays away from the type of exotic Spielerei described in this article.