C++ permission model
Abstract
The problems of public
, protected
and private
access are considered.
The generic templated access
approach with the on-demand specialization
for a consumer is proposed; it's advantages and drawbacks are discussed.
The synthetic solution satisfactory enough is proposed in the conclusion.
The problem
Out of the box C++ offers "classical" class access model: public
properties
(fields and methods) are accessible from anywhere, protected
properties
are accessible to descendant classes only, and finally private
properties
which permit access to class itself only.
Additionally it is possible to declare friend
class (which might be templated)
to provide maximum access to all properties (i.e. the same as private
). This
allows to access to the internals of a class to a related class.
For example, if there is an HTTP-library with Request
and Connection
classes
and Request
class would like to access Connection
internals, this can be done
as:
class Request; /* forward declare */
enum class Shutdown { read, write, both };
class Connection {
public:
virtual void handle() { ... }
private:
void shutdown(Shutdown how) { ...; }
int skt;
friend class Request;
};
class Request {
public:
virtual void process() {
...;
/* I know what I'm doing */
conn->shutdown(Shutdown::both);
}
protected:
Connection* conn;
};
Now let's assume that there is an descendant
class HttpRequest: public Request {
public:
virtual void process() override {
conn->shutdown(Shutdown::both); // Oops!
}
};
Alas, there is no way in C++ to access to Connection::shutdown
from it.
To overcome this, with the current access model, there are the possibilities.
First, it is possible to declare HttpRequest
as a friend in the Connection
.
Whilst this will certainly work, the solution has strict limitation, that it
applicable only for single library (project) to code of which you have access.
Otherwise, it does not scales at all.
The second possibility if is to "expose" private connection from the Request
class to all it's descendants, like:
class Request {
protected:
void connection_shutdown(Shutdown how) { conn->shutdown(how); }
int& connection_socket() { conn->skt; }
const int& connection_socket() const { conn->skt; }
Connection* conn;
};
This approach is better, because it scales to all descendant classes which can
be located in different libraries. However, the price is quite high as there
is need to provide access to all properties apriori even if some properties
will not be needed. The more serious drawback is that the approach is limited
to class inheritance; in other words, if there is need to access private
properties of Connection
not from Request
's descendants, e.g. for tests.
Somebody might become disappointed at all and try to make everything public by default. This scales well and covers all abovedescribed issues though brings a new ones: the boundary between stable public API interface and private implementation details of a class is blurred and completion suggestions in an IDE can be overloaded with too many variants. In other words the proposed approach is too permissive.
Semantically identical would be to write simple accessors for all private properties; it just brings an illusion of the interface/implementation separation since a class author already exposed all class internals outside.
Let's summarize the requirements for the private properties (aka implementation details):
they should scale outside of a library
they should be accessible outside of class hierarchy
they should not "pollute" the class public API, i.e. somehow be not available by default, still be accessible
The possible solutions
It consists of two pieces, the first one is to declare possibility to access the private fields of a class, e.g.:
// my_library.h
namespace my::library {
class Connection {
public:
virtual void handle() { ... }
template<typename T...> auto& access() noexcept;
private:
void shutdown(Shutdown how) { ...; }
int skt;
};
}
The second piece is actually provide full access specialization in the target place, e.g. :
// my_app.cpp
namespace to {
struct skt{}; // matches private field
}
namespace my::library {
auto& Connection::access<to::skt>() noexcept { return skt; }
}
// namespace does not matter
class HttpRequest: public Request {
public:
virtual void process() override {
auto& s = conn->access<to::skt>(); // voila!
shutdown(s, SHUT_RDWR);
}
};
In other words, in the source class the generic templated accessor is defined, and in the place, where the access is needed, the specialization is provided as the actual access to the required fields.
The solution meets all requirements, however it still has it's own drawbacks.
First, there is need of duplication of const
and non-const
access, i.e.
class Connection {
public:
virtual void handle() { ... }
template<typename T...> auto& access() noexcept;
template<typename T...> auto& access() const noexcept;
};
...
namespace my::library {
auto& Connection::access<to::skt>() noexcept { return skt; }
auto& Connection::access<to::skt>() const noexcept { return skt; }
}
Although, you don't have to provide const
and non-const
access if you need
only one.
The second drawback, that to let the approach work for methods, especially
those, which return type can't be auto&
(e.g. void
or int
). To overcome
it the access
should be rewritten as:
class Connection {
public:
template<typename T, typename... Args> T access(Args...);
}
namespace my::library {
void Connection::access<void, Shutdown>(Shutdown how) {
return shutdown(how);
}
}
class HttpRequest: public Request {
public:
virtual void process() override {
conn->access<void, Shutdown>(Shutdown::both);
}
};
Another problem arises: if there are two or more private methods with identical signatures (return and arguments types), the artificial tag should be introduced again, i.e.
class Connection {
template<typename T, typename Tag, typename... Args> T access(Args...);
};
namespace to {
struct skt{};
struct shutdown{};
}
namespace my::library {
int& Connection::access<int&, to::skt>() { return skt; }
void Connection::access<void, to::shutdown, Shutdown>(Shutdown how) {
shutdown(how);
}
}
...
conn->access<void, to::shutdown>(Shutdown::both); // voila!
The variadic Args...
template parameter dos not force to duplicate the original
arguments; it can have even add unrelated types to "inject" new methods with additional
logic into the Connection
class. For example:
namespace to {
struct fun{}
}
namespace my::library {
void Connection::access<void, to::fun>() {
Shutdown how = std::rand() > 1000 ? Shutdown::read ? Shutdown::write;
shutdown(how);
}
}
It is known, that methods might have optional noexcept
specification in addition
to const
. So, for the sake of generality, all four access cases should be
provided, i.e.:
class Connection {
public:
template<typename T, typename Tag, typename... Args> T access(Args...);
template<typename T, typename Tag, typename... Args> T access(Args...) const;
template<typename T, typename Tag, typename... Args> T access(Args...) noexcept;
template<typename T, typename Tag, typename... Args> T access(Args...) const noexcept;
};
Alas, it was not the last problem with the approach: there is a problem with inheritance, e.g.:
class Connection {
template<typename T> auto& access();
private:
int skt;
};
enum class SslState { /* whatever */};
class SslConnection:public Connection {
public:
template<typename T> auto& access();
private:
SslState state;
};
namespace to {
struct skt{};
struct state{};
}
namespace my::library {
auto& Connection::access<to::skt>() { return skt; }
auto& SslConnection::access<to::state>() { return state; }
}
However, as soon as try to access to parent property via child class, i.e.:
SslConnection* conn = ...;
auto& skt = conn->access<to::skt>(); // oops!
It cannot resolve access to socket via SslConnection
because there is no
to::skt
specialization for SslConnection
; there is on in it's parent class,
but in accordance with C++ rules a compiler does not see it. The solution
is to cast to the base class:
SslConnection* conn = ...;
auto& skt = static_cast<Connection*>(conn)->access<to::skt>();
This becomes even more unhandy when an object is stored behind smart pointer.
Let's enumerate key points:
accessors multiplication due to
const
andnoexcept
variantsnot so handy access for private methods (too verbose due to multiple template params), although "injection" of own accessor-methods seems an advantage
too clumpsy syntax to access private proreties in class hierarchy
Conclusion
The proposed solution is far from perfect. I found the following golden ratio for my projects on the implementation details access topic:
if the property is stable enough or it is the part of class interface, then public accessor should be written for it. It would be desirable for read only access, i.e. the accessor should be just a getter. For example, the
address
property inactor_base
in rotor.otherwise, if implementation details might be usable in descendants, make them
private
provide generic templated accessor (
template<typename T> auto& access()
) but for properties only; no access to private methods, as I don't see possible use cases now. This point might be different for different projects.
The described approach is applied in to be released soon
rotor v0.09
.