GCO4020/CSC428 - Advanced Object Oriented Techniques In C++
Week 1

 

Topic 2: Proxies

Introduction

C++ provides two inbuilt language constructs which can be used to facilitate indirect access to objects: references and pointers. However, both these mechanisms offer only the crudest level ("all or nothing") of access control.

Proxy¤ classes provide more flexible means of passing around, and controlling access to, reference information.


Synopsis


Source Code Examples


What is a Proxy¤?

A proxy¤ is an object which takes the place of (and often provides access to) another object. Proxies are a fundamental idiom and may be used in a variety of contexts:


Where are Proxies Needed?

Take, for example, a simple string class, based on null-terminated character arrays. Such a class might be declared thus:

        class String
        {
        public:
        	String(void);
        	String(char* cstr);
        	String(const String& str);
        	~String(void);

char& operator[] (int index); char operator[] (int index) const;

int length(void) const;

private: char* myStr; };

Consider the non-const version of String::operator[], which might be defined as follows:

        char&
        String::operator[](int index)
        {
        	if (index<0||index>=length())  { throw BadIndex(index); }
        	return myStr[index];
        }

This function first checks that the requested index is in range (throwing an exception if it is not) and then returns a reference to the appropriate char object stored in the array pointed to by myStr. This allows us to access and modify individual characters stored within the String, just as we can with an inbuilt character array:

        String s ("Hello world!");

cout << s[7] << endl;

s[1] = 'y';

There is, however a real danger in exposing the "raw" characters within the String in this manner. That danger is that someone will inadvertantly (or advertantly!) assign a '\0' within the String:

        s[4] = '\0';

thereby chopping off the rest of the characters. Note that the problem does not occur with the second (const) version of operator[], since it only returns a copy of the relevant character.

The problem can be solved by not returning a direct reference in the first place. Instead, we return a proxy¤ object that acts like a direct reference, except when it is assigned '\0', in which case it executes some error handling code (for example, throwing an exception).


Implementing a Proxy¤ for the String Class.

With a suitable proxy¤ class to guard its operator[], class String might be redefined like this (see source code listing: CharProxy.C):

        class String
        {
        public:
        	String(void);
        	String(char* cstr);
        	String(const String& str);
        	~String(void);

// PROXY CLASS FOR REFERENCES TO INDIVIDUAL CHARS WITHIN A String

class CharProxy { public: CharProxy(const char&); operator char& (void);

CharProxy operator=(const char&); CharProxy operator=(CharProxy); private: char& myRef; };

CharProxy operator[] (int index); char operator[] (int index) const;

int length(void) const;

private: char* myStr; };

Here, CharProxy is the proxy class, instances of which act as substitutes for the raw char&s used in the first version of the String class.

The constructor and operator char& functions permit CharProxys and char&s to be used interchangeably in most contexts, by effectively giving (indirect) access to the private myRef member:

        String::CharProxy::CharProxy(const char& ref)
        : myRef(ref)
        {}

String::CharProxy::operator char& (void) { return myRef; }

The two versions of operator= provide the desired control over assignments of '\0' by testing for that case and throwing an exception if it occurs:

        String::CharProxy
        String::CharProxy::operator=(const char& c)
        {
        	if (c=='\0')  { throw NullAssignment(); }
        	myRef = c;
        	return *this;
        }

String::CharProxy String::CharProxy::operator=(CharProxy c) { if (c.myRef=='\0') { throw NullAssignment(); } myRef = c.myRef; return *this; }

Once the proxy class is defined, the non-const String::operator[] function can be rewritten to return a temporary CharProxy containing a reference to the appropriate char:

        String::CharProxy
        String::operator[](int index)
        {
        	if (index<0||index>=length())  { throw BadIndex(index); }
        	return myStr[index];
        }

Note that, apart from its return type, this version of String::operator[] is textually identical to the previous version.

Now, when a piece of code like this is executed:

String s("text");

s[0] = '\0';

The following sequence of events occurs:

  1. The String object s is constructed.

  2. The member function String::operator[] is called on object s, and returns a CharProxy object containing a reference to the first element of s.myStr.

  3. The member function CharProxy::operator= is called on the temporary CharProxy object. It checks if the assigned value is '\0' (which it is) and hence throws a NullAssignment exception.

Note that if the assigned value had not been '\0', then CharProxy::operator= would have correctly assigned it (via its encapsulated reference member) to the first element of s.myStr.

See also: Exercise 1

See also: Exercise 2


Limitations of Proxies.

Proxies such as the one described above can improve the robustness of code, but cannot guarantee it. For example, it is easy to circumvent the protective (anti-'\0') mechanism explicitly:

        static_cast<char&>(s[0]) = '\0';	// NO EXCEPTION THROWN!

or, more subtly:

        void nullify(char& c)
        {
        	c = '\0';
        }

: :

nullify(s[0]); // NO EXCEPTION THROWN!

Another limitation of proxies becomes apparent when they are used to protect user-defined objects. Imagine a class storing a fixed set of objects, where the sum of some property of the objects must remain within some threshold. Such a class might be used to control the allocation of finite external resources like printers, hard disks, communication links, etc.) but we will consider it in the abstract here (see source code listing: Pool1.C):

class Device; // SOME PRE-DEFINED DEVICE CLASS

class Pool { public: Pool(void) : myActives(0) {}

class DeviceProxy { public: DeviceProxy(Device& dev, Pool& pool) : myRef(dev), myPool(pool) {}

operator Device& (void) { return myRef; }

void operator=(const Device& newdev) { int delta = (newdev.Active()?1:0)-(myRef.Active()?1:0); if (myPool.myActives+delta >= myPool.THRESHOLD) { throw PoolOverflow(newdev); } myPool.myActives+= delta; myRef = newdev; }

private: Device& myRef; Pool& myPool; };

friend class DeviceProxy;

DeviceProxy operator[](int index) { if (index<0||index>=MAXDEVICES) { throw BadIndex(index); } return DeviceProxy(myDevice[index],*this); }

private: enum { MAXDEVICES = 100, THRESHOLD = 20 };

Device myDevice[MAXDEVICES]; // DEVICES IN THIS POOL int myActives; // HOW MANY ARE ACTIVE? };

Here the proxy is being used to intercept the assignment of devices so as track (and limit) the number of concurrently active devices in any pool. This works well for assignments like this:

      Pool   pool;
      Device dev1;

dev1.Activate(); pool[7] = dev1;

but makes other activities on members of a pool awkward or even dangerous (see below). For example, if the Device class has a member function GetStatus(), we can say:

      int status = dev1.GetStatus();

but not:

      pool[7] = dev1;
      int status = pool[7].GetStatus();

This is because pool[7] returns a DeviceProxy object, which does not itself have a GetStatus() member function (and the rules of implicit casting don't allow the program to automatically invoke DeviceProxy::operator Device&(void) in order to get an object which does have that member function).

Hence, in order to access the status of the eighth device in the pool we have to be explicit:

      int status = static_cast<Device&>(pool[7]).GetStatus();

This is ugly and potentially confusing, so proxies like this sometimes provide an overloaded operator->() to simplify access:

      Device*
      Pool::DeviceProxy::operator->(void)
      {
            return &myRef;
      }

Now we can say:

      int status = pool[7]->GetStatus();

which is at least readable, if not wholly consistent with normal C++ usage.

Ideally, we would like to be able to overload DeviceProxy::operator.(), so that the use of the proxy is totally transparent to the user, but (alas!) operator.() cannot be overloaded (Stroustrup 1994, § 11.5.2)

More importantly, however, allowing access to the non-const members of class Device through the proxy is risky since it allows users to circumvent the limitations we wish to impose on Pool objects:

      pool[7]->Activate();      // OOPS! THIS ACTIVATION NOT RECORDED IN pool!

To avoid compromising the Pool class we may either choose not to make Device function members available through DeviceProxy::operator->(), or alternatively we may choose to extend DeviceProxy with its own Activate() function (see source code listing: Pool2.C):

class Pool { public: Pool(void) : myActives(0) {}

class DeviceProxy { public: DeviceProxy(Device& dev, Pool& pool) : myRef(dev), myPool(pool) {}

void operator=(const Device& newdev) { int delta = (newdev.Active()?1:0)-(myRef.Active()?1:0); if (myPool.myActives+delta >= myPool.THRESHOLD) { throw PoolOverflow(newdev); } myPool.myActives+= delta; myRef = newdev; }

bool Activate(void) { int delta = 1-(myRef.Active()?1:0); if (myPool.myActives+delta >= myPool.THRESHOLD) { return false; } myPool.myActives+= delta; myRef.Activate(); return true; }

private: Device& myRef; Pool& myPool; };

friend class DeviceProxy;

DeviceProxy operator[](int index) { if (index<0||index>=MAXDEVICES) { throw BadIndex(index); } return DeviceProxy(myDevice[index],*this); }

private: enum { MAXDEVICES = 100, THRESHOLD = 20 };

Device myDevice[MAXDEVICES]; // DEVICES IN THIS POOL int myActives; // HOW MANY ARE ACTIVE? };

so that now we can call:

      pool[7].Activate();

and have it correctly update the Pool::myActives count.

Note that Pool::DeviceProxy::Activate() does not throw an exception. Rather it returns false on failure. This is done so as to more closely mimic the (presumed) error-reporting behaviour of Device::Activate().

Note too that we have now removed both the DeviceProxy::operator->() and DeviceProxy::operator Device& () member functions, ensuring that Device objects returned via a DeviceProxy can only be accessed in by the methods defined in the DeviceProxy class.


Exercises

There are 2 exercises associated with this topic.

 


This material is part of the GCO4020/CSC428 - Advanced Object Oriented Techniques In C++ course.
Copyright © Damian Conway, 1997. All rights reserved.

Last updated: Fri Feb 18 11:17:03 2000