The access-by idiom in C++
The problem
Recently I had to write a cache manager in C++ for a problem-specific use case. To achieve that, I wrote a wrapper class around a map data structure. The cache manager had methods to store and access data using, surprise, keys! The important design decision about the cache manager class was that it hid the underlying map structure. When the time came to unit-test, I realized that a const reference to the underlying map object is needed, if desired to be rigorous about the tests.
I could think of two solutions: add a const method to the class that would return a reference to the underlying map, or add a friend class or a friend function thereby giving access to the private members to them. Neither option seemed to be a good approach to me.
-
Adding a const method to refer to the map would allow the user of the cache manager to bypass the checked-acces of the cached data. Which defeated the purpose of having the special cache manager in the first place.
-
On the other hand adding a friend function or a friend class to the class header just to be able to unit-test it, didn't feel right, as it implied the class is incomplete without implementing those friends.
Then I found this neat solution which I think, arguably, is an improved approach to the second option from above.
Solution
Let's look at it by writing an example. To keep things simpler, let's write a
basic cache manager class which caches std::string
data. The key used to
store and access data will be of size_t
type.
- Code language
- cpp
namespace nspc { // some namespace
class CacheManger {
private:
std::map<size_t, std::string> cache_;
public:
// constructors
// method: store data
// method: access data
template <typename T> struct access_by;
template <typename T> friend struct access_by;
};
} // namespace nspc
Now for unit-testing we sepcialize the access_by
struct from the
CacheManager
class, which we designed to be a friend class.
- Code language
- cpp
namespace nspc { // specialization happens in the same namespace
struct ConstAccessToCache{};
template <>
struct CacheManger::access_by<ConstAccessToCache> {
// A function that returns a const reference to the
// CacheManger's private member cache_
[[nodiscard]] auto const& cache(CacheManger& cm) {
return cm.cache_;
}
};
} // namespace nspc
A demo main.cpp
:
- Code language
- cpp
#include <string>
#include <map>
// CacheManger class definition from above
// CacheManger::access_by specialization from above
int main(int argc, char* argv[]) {
auto man = nspc::CacheManger{};
// access the cache_ member through the access_by specialization
auto accessor = decltype(man)::access_by<nspc::ConstAccessToCache>{};
auto const& cache = accessor.cache(man);
// now use cache for testing
return 0;
}