C++ Complementary Library
Published on July 31, 2024.
Written by Daniel T. McGinnis.
Accessors are functions that provide read or write access to a property of an object. A property is some characteristic about an object. Typically, in object-oriented code, data is hidden and public functions provide meaningful and controlled access to the internal state of an object. The two classic kinds of accessors are getters and setters, but CCL has a third kind, which I have not seen anywhere else, and I call them grabbers. To understand the motivation for grabbers, we must first see why getters and setters are not only useful, but sometimes necessary.
By using a getter and a setter to read and write a property of an object, you make it so that users of your class have to call a function to access that property. The benefit of a function is that it can perform computation. The classic example is that if you have a class called 'person' that provides a member function called 'get_age' that returns the person's age, you can change how the age is obtained without affecting client code. Users of your class continue calling the 'get_age' function even after you changed how the age is obtained. For example, you can make it so that the age is no longer represented by a data member, but is rather calculated based on the person's date of birth.
The more compelling argument for getters and setters is that sometimes they are required. For example, to obtain or change the title text of a window in a graphical user interface, you have to issue a system call (or issue a call to some system-provided API, like in the case of a Linux-based OS), so having a getter and a setter as functions, your users are able to read and write the title text of a GUI window. This shows that some properties fundamentally require computation to access. In the case of the title text of a GUI window, the title text fundamentally is read from and written to with operating system API calls.
Getters and setters are not without cons, and CCL addresses them with grabbers.
In CCL, getters always return a value, which means that if the property is represented by a private data member, then you get a copy of the member. Grabbers on the other hand return a reference to a private data member (essentially, they "grab" the original object), allowing outside code to directly manipulate the internal state of the object that the grabber is called on. This means that grabbers can only be made when the property is represented by a data member, unlike getters, which may compute their return value.
Grabbers are useful when a property is expensive to get and then set, and provides for a nicer syntax. For example, in the following code snippet we don't use a grabber, which results in unnecessary overhead. Instead, we copy a (potentially large) vector out of an object, manipulate it in some way (here we're just adding an element), and we then copy the vector back into the object.
auto children{some_object.get_children()};
children.push_back(some_new_child);
some_object.set_children(children);
This is inefficient. Using a grabber would solve that inefficiency:
some_object.grab_children().push_back(some_new_child);
Grabbers also result in nicer-looking code. For example, this:
some_object.grab_name() += u8"Additional words";
is easier on the eyes than this:
some_object.set_name(some_object.get_name() + u8"Additional words");
One might argue that instead of providing a function that returns a reference to a private data member, you're better off just making the data member public. The problem with that is consistency. By establishing the concept of getters, setters and grabbers, users of your API will always know how to access properties on an object, and won't have to guess (do I call a function or assign a data member?) and they will get more useful autocomplete in code editors.
Because some properties can only be implemented as getters and setters, it follows that using getters, setters and (when possible) grabbers for all properties (including in trivial cases) is beneficial to the consistency of your API, making it easier to use.
The distinction between getters that always return a value and grabbers that always return a reference makes it easy to see when a copy is being made, which in turn makes the code easier to optimize, whereas if some getters return a value (to enable the computation of the information) and others return a reference (to enable efficient manipulation of the original object), it becomes harder to reason about the performance of the code.
By adopting this approach based on getters, setters and grabbers, you get the ability to make write-only properties. You just provide a setter and no getter or grabber. Uses for write-only properties aren't very common, but if you do need this capability, remember that C++ doesn't natively provide it like it natively provides a way to specify that an object is constant (read-only) or non-constant (readable and writable).
Hopefully I have made the case for getters, setters and grabbers, which I believe are the best way to control access to properties in C++ today (maybe one day C++ will have a property syntax built into the language). The gain in maintainability (for you) and usability (for your API's users) are the main selling points.
That does it for now. Keep being awesome!
Copyright © 2022-2025 Daniel T. McGinnis