![]() | |
![]() ![]() |
![]() | Imperfect C++ Practical Solutions for Real-Life Programming By Matthew Wilson |
Table of Contents | |
Chapter 35. Properties |
35.3. Field PropertiesThere are four kinds of field properties, representing read or write access, and internal or external implementation. These are represented in the four classes, field_property_get, field_property_set, field_property_get_external, and field_property_set_external, which we'll look at now. Obviously there's no point combining field property get and set accesses, for example, field_property_getset and field_property_getset_external, since that is semantically equivalent to a public variable, and would be a grand waste of effort. 35.3.1 field_property_getLet's look at how we might implement a read-only field property. Consider that we have a linked list class, and we want to provide a Count property, to indicate how many items are in the list. In any sane list implementation,[3] the count of items is maintained as a member variable, as opposed to conducting a count by walking the list. Since it makes no sense to assign to the count of a list, Count will provide a read-only view onto a private integer member variable. We could implement this as shown in Listing 35.4.
Listing 35.4.class ListElement; class LinkedList; class LinkedListCountProp { public: operator int() const { return m_value; } private: friend class LinkedList; // Allow LinkedList access to m_value int m_value; }; class LinkedList { // Properties public: LinkedListCountProp Count; // Operations public: void Add(ListElement *e) { . . . ++Count.m_value; // Update the count } }; The property Count is defined as of type LinkedListCountProp, and is a public member variable of LinkedList. It looks a little strange, since there's no m_ prefix,[4] and it's uppercase. This is my naming convention for properties, in order that they are disambiguated from member variables; you're free to choose your own. For example, if you need them to appear as public member variables, for generalization, then they'd be defined according to the convention of the type(s) you're emulating.
Client code can refer to the Count member of instances of LinkedList, and in that way access the (int) value, via the implicit conversion operator. Client code cannot change the value of LinkedListCountProp::m_value because it is private, but the friendship declaration means that it can be altered by Rectangle. Simple really, isn't it? The downside is all too clear. LinkedListCountProp is a purpose-written class, and an equivalent would be required for every containing class and every value type. Unless we want a lot of typing, code-bloat, and maintenance headaches, we want a generalized solution. Naturally, templates are the answer, in the form of field_property_get, which is shown in Listing 35.5. Listing 35.5.template< typename V /* The actual property value type */ , typename R /* The reference type */ , typename C /* The enclosing class */ > class field_property_get { public: typedef field_property_get<V, R, C> class_type; private: // Does not initialize m_value field_property_get() {} // Initialises m_value to given value explicit field_property_get(R value) : m_value(value) {} DECLARE_TEMPLATE_PARAM_AS_FRIEND(C); public: /// Provides read-only access to the property operator R () const { return m_value; } private: V m_value; // Not to be implemented private: field_property_get(class_type const &); class_type &operator =(class_type const &); }; The template takes three parameters: the type of the internal member variable, the reference type by which its value may be accessed, and the type of the containing class. The separation of value type and reference type increases flexibility and efficiency. For example, if you were using std::string as the value type, you would likely choose to use std::string const& as the reference type. As with the specific case of LinkedListCountProp, read-only access is achieved by making the member variable m_value and the constructors private, but providing a public implicit conversion operator to the reference type. Note that the default constructor does not perform explicit initialization of the member variable. This is in common with my personal predisposition to making efficiency a high priority, and that unnecessary initialization is a waste of time. Initialization of the value is achievable by the nondefault constructor. You may take a different point of view, and therefore implement the default constructor to also explicitly default construct the member variable. field_property_get() : m_value(V()) {} As before, the containing type needs write access to the member variable, which is provided by granting it friendship of the template using the technique we saw in section 16.3. Using this general class template, we can now define LinkedList as:
class LinkedList
{
// Construction
public:
. . .
// Properties
public:
field_property_get<int, int, LinkedList> Count;
};
There's an added bonus in that the declaration of the property Count is also self-documenting: it is a field-based read-only (get) property, whose value and reference types are both int. Client code expressions are also readable and unambiguous: LinkedList llist(...); for(. . .) { . . . // Add items to the list } int count = llist.Count; // Gives the count of items llist.Count = count + 1; // Error! Write access denied The copy constructor and copy assignment operator are hidden to prevent nonsensical use such as the following: field_property_get<int, int, Count> SomeProp(llist.Count); // Copy ctor llist.Count = llist.Count; // Copy assignment Hopefully you'll agree that we've achieved read-only access in a very simple fashion. On all the compilers (Appendix A) tested, the field_property_get template causes no speed or space overhead, so it is 100 percent efficient. The only downside is that it uses the currently nonstandard, though very widely supported, mechanism of granting friendship to a template's parameterising type.[5] But we're imperfect practitioners, and we want something that works (and is portable). This does (and is).
35.3.2 field_property_setI must say that I cannot think of any use for a write-only field property, but we're going to look at the mechanism for pedagogical purposes, as this will inform on our implementation of the write-only method properties (see section 35.4), which do have valid applications. Write-only properties are the semantic opposite of read-only, so the field_property_set template replaces the public implicit conversion operator with an assignment operator, as in: Listing 35.6.template< typename V /* The actual property value type */ , typename R /* The reference type */ , typename C /* The enclosing class */ > class field_property_set { public: typedef field_property_set<V, R, C> class_type; private: field_property_set() {} explicit field_property_set(R value) : m_value(value) {} DECLARE_TEMPLATE_PARAM_AS_FRIEND(C); public: /// Provides write-only access to the property class_type &operator =(R value) { m_value = value; return *this; } private: V m_value; // Not to be implemented private: field_property_set(class_type const &); class_type &operator =(class_type const &); }; This template has the same efficiency, portability, and legality attributes as its read-only brother. Everything gets inlined, and it's conformant code apart from the (highly portable) friend relationship. 35.3.3 Internal Fields Properties: CodaWe've now seen the core aspects of the property technique. Read-only access is achieved by providing a public implicit conversion operator in the property class, and hiding the copy constructor and copy assignment operators to prevent misuse. The containing type is made a friend of the property so that it can set the value, which is otherwise inaccessible due to being declared private. Write-only access is achieved by providing a public assignment operator, and not providing an implicit conversion operator. The containing type is once again made a friend, this time so it can access the value of the property. For the ultra-cautious (which includes me) it is tempting to declare, but not define, a private implicit conversion operator. We'll see why this is not wise shortly. For the remaining permutations we'll take these aspects as read (pardon the pun!) and focus on the individual mechanisms. 35.3.4 field_property_get_externalThe internal field properties are the best option if we have full control over the implementation of the containing class to which we are adding the properties. However, sometimes this is not the case, and what we seek to do is augment a class with properties based on existing member variables. This is achieved using a reference. Hence, field_property_get_external is defined as: Listing 35.7.template< typename V /* The actual property value type */ , typename R /* The reference type */ > class field_property_get_external { public: field_property_get_external(V &value) // Takes a reference : m_value(value) {} // Accessors public: /// Provides read-only access to the property operator R() const { return m_value; } // Members private: V &m_value; }; Ostensibly, this property looks simpler than the internal equivalent. field_property_get_external is parameterized with the value type and the reference type to the variable, and holds a reference to the value type, which is passed in its constructor. Since the property object holds a direct reference to the member variable for which it acts, it does not need to know, or be friends with, the containing class. Hence, the template is 100 percent language compliant. Furthermore, it presents no challenge to even very template-deficient compilers; even Visual C++ 4.2 compiles this! In terms of speed efficiency, the template is amenable to even cautious optimization, such that the notional indirection through the reference can be optimized into a direct reference. The downside is that the reference results in space inefficiency: on 32-bit machines, the reference costs 4 bytes per property. Furthermore the property is a little more complex to use, since it must be initialized with a reference to the variable for which it will act. Listing 35.8.class LinkedList { // Construction public: Rectangle(. . . : Count(m_count) , . . . // Properties public: field_property_get_external<int, int, LinkedList> Count; // Members private: int m_count; }; At first glance, this looks like a member initializer list ordering problem (see section 2.3), but in fact it's not. The reason is that Count is passed m_count as a reference, not as a value. Hence, the only way in which this could itself be an ordering problem would be if Count was being used in another initializer, which would be an ordering problem all its own and doesn't have anything to do with the property per se. Nonetheless, it's worth pointing out, since member ordering dependencies are in principal bad things, and you should be mindful of the pitfalls whenever you (think you) see them. 35.3.5 field_property_set_externalfield_property_set_external provides the semantics of field_property_set with the same reference mechanism as field_property_get_external. 35.3.6 Hacking AwayWhen we look at method properties in the next section we're going to see that there's a way to hack away at the space inefficiencies by using esoteric techniques. While it's possible to do the same for external field properties, I've not done so simply because they are so very rarely used. Of course, they may be so rarely used because they have the space cost, but we're getting philosophical, so you take the chicken and I'll take the eggs, and we'll move on. |
![]() | |
![]() ![]() |