Here is a very minimal class that can be used to wrap an attribute so that syntactically it will behave as if it has accessors.
template< typename T > class Accessors { private: T m_t; public: Accessors() {} Accessors( const T &t ) : m_t( t ) {} const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } };
This is the absolute simplest implementation that I can think of and should be a good starting point. Here's how it can be used.
struct SomeObject { Accessors< int > an_int; Accessors< std::list< int > > some_ints; } an_object; void f() { an_object.an_int( 3 ); int i = an_object.an_int(); } void g() { std::list< int > ints = an_object.some_ints(); ints.push_back( 3 ); an_object.some_ints( ints ); }
As we can see from SomeObject
the Accessors
class pretty neatly encapsulated the accessors just as we want, and in f()
we can see it works very well with a POD (the int
). It really does suck for the std::list<>
though.
Nasty as it is for larger data structures (those that are expensive to copy) I want to fix something else first. At the moment we could change f()
to this:
void f() { an_object.an_int = 3; int i = an_object.an_int(); }
This is really nasty. The whole point of the class is to allow us to use the same syntax as we'd use for accessors without actually having to write them. This isn't doing that.
As the exploitation relies on a copy we could disallow copy on Accessors
. The standard way to do this is to make both the copy constructor and the assignment operator private and not provide implementations for them:
template< typename T > class Accessors { private: T m_t; public: Accessors() {} Accessors( const T &t ) : m_t( t ) {} const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } private: Accessors( const Accessors & ); Accessors &operator =( const Accessors & ); };
We now get a compile error within f()
, but we've now also stopped SomeObject
instances from being copyable so this is not a good solution. A better one is to make the Accessors
constructor which takes a value explicit.
template< typename T > class Accessors { private: T m_t; public: Accessors() {} explicit Accessors( const T &t ) : m_t( t ) {} const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } };
We now get an error when compiling f()
, but we've preserved the copy capabilities of SomeObject
. It is possible to do this in f()
though:
void f() { an_object.an_int = Accessors< int >( 3 ); int i = an_object.an_int(); }
I'm not going to worry about that. The idea here is to help us write less code and if people want to do all of that typing I'll let them.
So let's take a look again at the list of int
s. If I was writing the accessors by hand I'd write two getters and no setter like this:
std::list< int > &some_ints(); const std::list< int > &some_ints() const;
We should clearly do the same thing in Accessors
, but include the setter because we want it for the int
.
template< typename T > class Accessors { private: T m_t; public: Accessors() {} explicit Accessors( const T &t ) : m_t( t ) {} T &operator() () { return m_t; } const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } };
The problem is that we now have an odd hybrid accessor pattern. The implementation is alright, but not as good as we could have it with a bit more effort.
What we need to do is to have two implementations of Accessors
, one for simple data types and one for complex data types:
template< typename T > class Accessors_lvalue { private: T m_t; public: Accessors_lvalue() {} explicit Accessors_lvalue( const T &t ) : m_t( t ) {} T &operator() () { return m_t; } const T &operator() () const { return m_t; } }; template< typename T > class Accessors_rvalue { private: T m_t; public: Accessors_rvalue() {} explicit Accessors_rvalue( const T &t ) : m_t( t ) {} const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } }; struct SomeObject { Accessors_rvalue< int > an_int; Accessors_lvalue< std::list< int > > some_ints; } an_object;
That we have to use two names is annoying, but at least g()
now looks much more sensible:
void g() { an_object.some_ints().push_back( 3 ); }
What if we could tell the implementation what we wanted?
enum Accessor_type { rvalue, lvalue }; template< typename T, Accessor_type > class Accessors;
It turns out that this is exactly what we can do. What we're going to use is something called partial template specialisation. We're going to fix the second template parameter, but still allow people using our class to specify the first.
We start off with the normal template
pre-amble, but now we are going to be using the template class we declared in order to define two versions of it. This means we have to specify it in the same way that we do when we use it anywhere else. This gives us these two implementations:
template< typename T > class Accessors< T, lvalue > { private: T m_t; public: Accessors< T, lvalue >() {} explicit Accessors< T, lvalue >( const T &t ) : m_t( t ) {} T &operator() () { return m_t; } const T &operator() () const { return m_t; } }; template< typename T > class Accessors< T, rvalue > { private: T m_t; public: Accessors< T, rvalue >() {} explicit Accessors< T, rvalue >( const T &t ) : m_t( t ) {} const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } };
Take a think about why we have to refer to the Accessors
class like this. We are using the class to define two alternative versions of it. Each of these alternatives now takes a single type as a template parameter and either lvalue
or rvalue
depending on which implementation we want to have.
Now when we define SomeObject
we also choose which sort of accessor to use:
struct SomeObject { Accessors< int, rvalue > an_int; Accessors< std::list< int >, lvalue > some_ints; } an_object;
This works much better. We now get the exact implementation that we want¹ [1If there are other accessor patterns that you have you can include then in the enumeration and add specialisations for them. You can go as crazy as you like, but too much choice might be too hard to keep track of and choose between.].
There is one final tweak we can make. Most of the time I'm going to want to use this class it's to wrap some simple data type, often whilst I first work on a problem and before I find the need to do anything more sophisticated. This means that I'm nearly always using the rvalue
option² [2It might be possible to use some much more complex template programming to get the compiler to automatically choose the right one, but it isn't something I've felt the need to explore yet.]. It'd be great if we could set that to the default:
template< typename T, Accessor_type = rvalue > class Accessors;
And of course yet again we can do exactly that. SomeObject
becomes a little bit neater now.
struct SomeObject { Accessors< int > an_int; Accessors< std::list< int >, lvalue > some_ints; } an_object;
Notice how the declaration for an_int
looks like it did for our first version before we dealt with the special case of the std::list<>
? I think that's pretty neat.
Here's the final implementation of the Accessors
template for the two accessor patterns we identified:
enum Accessor_type { rvalue, lvalue }; template< typename T, Accessor_type = rvalue > class Accessors; template< typename T > class Accessors< T, lvalue > { private: T m_t; public: Accessors< T, lvalue >() {} explicit Accessors< T, lvalue >( const T &t ) : m_t( t ) {} T &operator() () { return m_t; } const T &operator() () const { return m_t; } }; template< typename T > class Accessors< T, rvalue > { private: T m_t; public: Accessors< T, rvalue >() {} explicit Accessors< T, rvalue >( const T &t ) : m_t( t ) {} const T &operator() () const { return m_t; } void operator() ( const T &t ) { m_t = t; } };
It isn't perfect, but it is going to save you a lot of typing and refactoring effort. And even better than the saved typing is the fact that the run-time cost on a release build will be exactly zero³ [3Assuming you're not using a completely brain-dead compiler anyway.].
Two final points.
And finally, for const
members make the type inside Accessors
const
rather than the Accessors
object. I'll leave it to you to work out reasons for this.