The A::Restricted idiom
Posted: December 31, 2013 Filed under: C++, Software engineering | Tags: C++, Idiom, Software engineering Leave a comment
Fine grained access control to private members of a class
Sometimes I wish I could control the access to a class in a finer way.
Usually you have these tools in your arsenal:
- private
- protected
- public
- friend
Private is fine, since most of the stuff in a class should be hidden from all, in order to maximize encapsulation.
Protected parts can be accessed by anybody who derives from your class. This opens your class too much, i.e. to every deriving class. Making these protected parts private later on can be impossible due to the number of deriving classes or due to not knowing who may derive from you (e.g. if you are a library). This can considerably hinder refactoring efforts.
Public parts have the same problems as the protected parts and more. The whole world can see your public parts.
Making a non-related entity a friend of your class also exposes too much. The friend can access every part of your class even if it does not need to.
The bottom line is, if you open up your class with protected or public then the encapsulation of your class is hurt badly. Also, making non-members as friends is almost always unnecessarily generous.
We would need better support in the core language for more specific control over who can access and what. Something like this:
// WARNING! This is NOT C++! The "public(...)" is fictional. class A { public(class B, class D): // Private for all but public for B and D. void f(); private: // Private for all. int a; }; class B : public A { void h() { f(); } // OK! A::f() is public for B. }; class C : public A { void h() { f(); } // Error! A::f() is private for C. }; class D { void h(A& a) { a.f(); } // OK! A::f() is public for D. };
In the above code, A::f() is marked as public only for classes B and D. For everybody else, it is private (default access for class members).
Unfortunately, the public access modifier in C++ does not support this syntax and semantics.
Luckily, there is a workaround to emulate this kind of behavior. Here is one way to do this:
// a.hpp #pragma once class A { private: void f(); virtual void g(); double d; public: A(); class Restricted { private: A& parent; Restricted(A& p); // Proxy functions void f(); void g(); // Friends of Restricted friend class A; friend class B; friend class D; }; Restricted restricted; };
// a.cpp #include "a.hpp" #include A::A() : restricted{*this} { } void A::f() { std::cout << "A::f()" << std::endl; } void A::g() { std::cout << "A::g()" << std::endl; } A::Restricted::Restricted(A& p) : parent{p} { } void A::Restricted::f() { parent.f(); } void A::Restricted::g() { parent.g(); }
Class A has some private parts, f, g and d. From these parts, we want to expose only the functions and we want to strictly control who can access them.
For this, we define an inner Restricted class. In Restricted, everything is private. We open it up only for selected entities; here A, B and D. We create proxy functions whose task is to forward the call to those parts in A that we want to make accessible to the friends of Restricted. The Restricted class has a reference to the outer A object to which it forwards the calls.
The friends of Restricted can access Restricted::parent, but this is not a problem at all. The parent is a reference, so it cannot be changed to point to some other A after construction. Also, only the public parts of A can be access through parent. The encapsulation of A is not weakened.
The friends of Restricted can access Restricted::Restricted(A&) and construct Restricted objects, but this is not a problem either. In the worst case, this can result in multiple Restricted objects referencing the same A object. Again, the encapsulation of A is not weakened because through these Restricted::parent references only the public parts of A can be accessed. Also, through these Restricted objects only the selected private parts of A can be accessed (here A::f() via A::Restricted::f() and A::g() via A::Restricted::g()).
In class A, everything is private and only the very minimum is made public. A::A() is public because we want to allow anybody to create A objects. A::restricted is public because otherwise the friends (e.g. B) specified inside Restricted cannot access it.
The examples below show how class A can be used. Access control to A::Restricted is managed strictly by A via the friends of A::Restricted. So, nobody else can gain access without the “permission” of A.
We create a class B deriving from A. B is a friend of A::Restricted. This gives B access to the restricted parts of A.
// b.hpp #pragma once #include "a.hpp" class B : public A { public: void h(); private: virtual void g() override; };
// b.cpp #include "b.hpp" #include void B::h() { std::cout << "B::h() enter" << std::endl; // ++d; // Error! A::d is private. // f(); // Error! A::f() is private. g(); // OK! B::g() is accessible here. restricted.f(); // OK! A::Restricted::f() is public here. restricted.g(); // OK! A::Restricted::g() is public here. std::cout << "B::h() exit" << std::endl; } void B::g() { std::cout << "B::g()" << std::endl; }
We create a class C deriving from A, but we do not give it access to the restricted parts of A.
// c.hpp #pragma once #include "a.hpp" class C : public A { public: void h(); };
// c.cpp #include "c.hpp" void C::h() { // f(); // Error! A::f() is private. // g(); // Error! A::g() is private. // restricted.f(); // Error! A::Restricted::f() is private. // restricted.g(); // Error! A::Restricted::g() is private. }
We create a class D that is not related to A. Yet, it can access the restricted parts of A because we explicitly allow it.
// d.hpp #pragma once class D { public: void h(class A&); };
// d.cpp #include "d.hpp" #include "a.hpp" void D::h(A& a) { // a.f(); // Error! A::f() is private. // a.g(); // Error! A::g() is private. a.restricted.f(); // OK! A::Restricted::f() is public here. a.restricted.g(); // OK! A::Restricted::g() is public here. }
// main.cpp #include "b.hpp" #include "c.hpp" #include "d.hpp" int main() { auto b = B{}; b.h(); // b.f(); // Error! A::f() is private. // b.g(); // Error! B::g() is private. // b.restricted.f(); // Error! A::Restricted::f() is private. auto c = C{}; c.h(); auto d = D{}; d.h(b); return 0; }
In this example, only B and D can access A::f() and A::g(), but only through A::Restricted. Nobody else can. A::d remains private for everybody. And this I call the A::Restricted idiom.
Here is the output of this example program:
B::h() enter B::g() A::f() B::g() B::h() exit A::f() B::g()
This idiom is a variation of the Attorney-Client Idiom. I prefer this variant (i.e. A::Restricted) because it provides a more convenient and more intuitive syntax for accessing the restricted parts. This is achieved by the automatic wiring between class A and class Restricted.
References
END OF POST