Link to home
Start Free TrialLog in
Avatar of mackayd
mackayd

asked on

Use difference functions on RHS and LHS of assignment

I recall seeing somewhere how you can create two overloaded functions, 1 called on the LHS and the other on the RHS of an assignment.

I want an 'operator[](...)' function that only references existing items when its used on the RHS of an assignment, but another one (obviously the same name etc) that can create a new entry if there is no matching item found when used on the LHS of an assignment.

I think it has something to do with the placement of 'const' keywords, but I can't make it work and I can't find any references on the web.

Thanks

Don
Avatar of jasonclarke
jasonclarke

You can define const and non-const versions of operator [] on the same thing, which is probably what you are referring to.  

An example:

class X {
public:
//...
   Element& operator[](size_t i);
   const Element& operator[](size_t i) const;
//...
};

However I am don't think this will do what you want since it will just call the operator based on the constness of the calling object.

You can overcome this problem using proxy classes, but that requires a bit more work.  Scott Meyers describes this approach if you have his book.

"jasonclarke" has it right!!!  The example he gave is CORRECT for what you want.

Using the example he has given, non-const object 'Element&' will allow subscripts as lvalues in expressions and eliminate calls to copy initializers.

For constant object, 'const Element&' makes subscripts legal ONLY as rvalues; using it as lvalues for constant objects will generate compilation errors (though Scott Meyers shows you how to get around this also).

const String  s = "Hello";
cout << s[0];   // rvalue - displays 'H'
s[0] = 'J';     // error - cannot use as lvalue due to const

String  a = "hello",  b = "jello";
a[0] = b[0];  // lvalue and rvalue
a[0] = 'H';   // lvalue
a[99] = 'M';  // lvalue - out of bounds
cout << a[99] << endl;   // rvalue - out of bounds
The problem with it is that if you call the operator as an rvalue on a non-const object, the non-const operator[] will be called.  So:

String s;

cout << s[0];  // non_const [] will be called

This may or may not be a problem.  

It might be a problem, for example, if you are doing this for efficiency and providing a more efficient const method that has to do less work.

An excellent example of this is a reference counted string class.  When [] is called for an lvalue, the target string must be copied before the write can be done,  but if used on an rvalue the copy is not required.  
If I am not too off the mark, the idea is to have two overloaded 'operator[]' functions:  one for LHS activities, the other for RHS activities.  Next, the idea is to have the compiler automatically call whichever is the proper one for the activity at hand.

When the activity involves a constant object, the compiler will call the RHS one, and when the activity involves a non-constant object, the compiler will call the non-const overloaded version.  What's wrong with that?

The main idea is for the programmer to provide a version for LHS operation when that is what is being done, and to also provide a version sensitive when working with 'const' object.  Once the programmer has fulfilled his part of the obligation, the compiler is the one that'll choose which one it wants to use according to the rules of C++, when working with the program code.

If the programmer uses a non-const object for a RHS operation, it's the 'non-const' overloaded version that'll be called (what's wrong with that?).  The main thing is, there will be a version available when dealing with 'const' objects for its own RHS operation; the 'non-const' version is NOT going to be the one called.
> If the programmer uses a non-const object for a RHS operation, it's the
> 'non-const' overloaded version that'll be called (what's wrong with that?).  

Its important in something like the reference counted string class, for example, a possible partial implementation could look like:

class String {
// The const version is simple
const char& operator[](size_t i) const
{
   return mStringRep->str[i];
}

// The non-const version is not...
char& operator[](size_t i)
{
   if (mStringRep->refcount > 1)
   {
       --mStringRep->refcount;
       mStringRep = new StringRep(mStringRep->str);
   }
   return mStringRep[i];
}

//...
};

can you see that in this case just because your non-const string is being used as an r-value, you wouldn't want all that extra work to be done - its not required - and is memory and speed inefficient. If your access to the string is read-only, you don't need to bother doing the copy on write stuff - but non-const operator[] does not know how it is being called.

The way around this is to use something like Scott Meyers proxy classes.    


 
Let us agree on certain basic points.

When 'operator[]' is being overloaded, it must be able to appear on either the LHS or the RHS in an assignment statement.  To accomplish this, it must return a reference.  However, because of the rule of C++, it cannot appear on the LHS if it's dealing with a constant object, hence a second version of overloading the '[]' operator must be provided; a version that will guarantee it can only be used as a RHS expression (so long as you don't violate any of the C++ rules and don't purposely go out of your way to show how well you can thwart the rules).

That's it!!  C++ has stated its rules.

Now, if you were to say, "Hold on!  That rule will impede me from being more effecient in my usage of processing time and memory."  That's a different story, because this is where your creativity comes in to see how well you can address those points, without violating any of C++ rules.  But then you're getting into a whole different area.  You are now getting into an area where optimization is your primary concern, and that is not what "mackayd" originally asked about.  He just wanted to know how he should go about defining 'operator[]' so that the compiler will automatically use the correct version when dealing with a LHS expression from when dealing with a RHS expression.

Your point about the reference counting is well taken, but perhaps (in this instance), reference counting is not what "mackayd" will be dealing with.
Avatar of mackayd

ASKER

Thanks for all of the comments.

Firstly, could someone pelase give me the full reference to the "Scott Meyers" book that has been mentioned: I think that the 'proxy class' approach may be what I'm after, given the discussion so far.

What I have is an associative array which I want to access using the 'operator[]' semantics. If I reference an element that does not exist by a LHS expression (eg Fred['NewValue'] = ...) then I want "Fred" to be extended to have a new element called 'NewValue'. (If an element called 'NewValue' already exists, then this will be replaced in this case).

However, if I reference a non-existant entry in the array from the RHS, then I want to throw an exception:

Fred['OldValue'] = 123;
AllOk = Fred['OldValue'];
// AllOk should now equal 123
NotOk = Fred['BadValue'];
// Should throw an exception as 'BadValue' doesn't exist yet

As you can see, I'm not dealing with a "const" and a "non-const" object to base the selection on. Therefore, if I understand you all correctly, I must use the proxy-class approach.

Thanks

Don

PS - when you reply, could someone please answer rather than comment so that I can give them the points.
ASKER CERTIFIED SOLUTION
Avatar of jasonclarke
jasonclarke

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Should have said both 'Effective C++' and 'More Effective C++' in the previous answer.
STL might be what you want.  It will automatically grow a 'new entry' in an array for you (that you didn't have before) if that's what you need.

"jasonclarke's" example includes a sample use of the STL vector container together with its 'push_back()' algorithm.

While I am not discounting "jasonclarke's" offer of a solution, it is somewhat advanced in that it encompasses two different techniques usually reserved for advanced programmers:  STL and Proxy Classes.

For example, on page 223 of Scott Meyers book, "More Effective C++" where he (Meyers) is talking about Proxy Classes, he also list the 'Limitations' for using Proxy Classes as a "technique not without its drawbacks, ... because objects are used as lvalues in contexts other than just assignment, and using proxies in such contexts usually yields different behavior than using real objects."  (IOW, not always what you see, is what you get.)

"jasonclarke" is a good programmer and he has done enough work to merit the points.  I just feel you will need to ask more questions before finally feeling comfortable with an answer.
Avatar of mackayd

ASKER

Thanks to "jasonclarke" and all of the others - I'll look at the code and try to get hold of the recommended books.

My use of the STL was only to facilitate the demonstration of the Proxy class technique - it seemed the simplest way to get what was required.

If the STL was being used, then the simplest thing would be to avoid using your own class and use the STL map which behaves similarly anyway.

I agree that this technique is not perfect,  and it means you may have to think quite carefully when writing code that uses the classes to check that the right thing is happening.
My use of the STL was only to facilitate the demonstration of the Proxy class technique - it seemed the simplest way to get what was required.

-----------------------------------

Not true!  Not true at all!  I will differ with you strongly here "jasonclarke".

The primary purpose of using a Proxy Class is to hide implementation details of a class to prevent access to proprietary information (including 'private' data) and proprietary logic in a class.  Moreover, it DOES NOT need the use of the STL to demonstrate the technique.

Here is a sample class whose data I am most desirous of hiding (in this class I could also include functions I may definitely wish to hide from others).

This is the sample class (hidden away some place in a separate file and not shown):


class  Implementation
{
 public:
    Implementation(int v)
    {  value = v;  }
    void setValue(int v)
    {  value = v;  }
    int getValue()  const
    {  return value;  }

 private:
    int  value;   // this is my proprietary data
};

-------------------------------

Here is my Proxy Class (preceded by Implementation class I am using for forward declaration purpose only):

class  Implementation;   // forward declaration purpose

class  Interface  // proxy class
{
 public:
    Interface(int);
    void setValue(int);
    int getValue()  const;

 private:
    Implementation *ptr;   // reason for the forward declaration
};

-----------------------------------

#include "interface.h"
#include "implementation.h"

Interface::Interface(int v) : ptr(new Implementation(v))
{  }

// call Implementation's setValue function.
// IOW, calling that which is hidden from sight

void  Interface::setValue(int v)
{  ptr->setValue(v);  }

// call Implementation's setValue function
// Likewise, calling that which is hidden from sight

int  Interface::getValue()  const
{  return ptr->getValue();  }

---------------------------------

// Hiding a class's private data with a proxy class

#include <iostream.h>
#include "interface.h"

int main()
{
   Interface  i(5);
   cout << "Interface contains: " << i.getValue()
        << " before setValue" << endl;

   i.setValue(10);
   cout << "Interface contains: " << i.getValue()
        << " after setValue" << endl;

   return 0;
}

----------------------------------

There is my Proxy Class, without the use of STL.


I'm a little confused about something, however.  First you claimed that overloading the "[]" operator twice (once for 'const' objects, and once for 'non-const' object) was inefficient due to situation such as reference counting.  Then, here you end up overloading it once, and in the process, using some rather complicated techniques that are very advanced so that persons not as technical as you or I, would find it difficult to grasp.

Then throughout your sample you qualified every occurrence of an STL container with the "std" qualifier, when a simple "using namespace std" following your "#include" statements would have been just as effective and much simpler.

Why do you feel you have to be overly complicated in order to show erudition?

===============================


Thanks to "jasonclarke" and all of the others

----------------------------

I don't see anybody else other than "jasonclarke" and myself.
Clearly the proxy technique can be simply demonstrated without the use of the STL,  however...

I was trying to show how the Proxy technique could be used to implement an associative array.

And as such, the STL vector seemed a simple way implement a dynamic array of objects that could be used in an associative array,  that was the whole reason for using the STL stuff.

I didn't overload the const version of operator[] because it would be unused in my example, and hence I thought possibly confusing. The implementation would be the same as for operator int in the proxy, however, and it could return int rather than a proxy.

I never use the 'using' directive in a header file, because it seems unnecessary to inflict the namespaces I choose to use on users of my classes if they don't wan't to use them explicitly.

And, while your example does demonstrate a use of the Proxy technique,  I think its a different variant from the one that is  required here.  The question clearly wants the kind of transparent proxy (i.e. its presence is transparent to users of the class) that my example demonstrates.

And finally, though this is really a matter of opinion, I believe that the STL is now a fundamental part of the language,  so objecting to its use would be a bit like objecting to the use of classes or the 'if' keyword.
The STL was not included in the Standard until mid 1994.  IOW, it has only been six years since its acceptance, and about two years since compilers began widespread incorporation of it.  IOW, it is still very new!

OTOH, classes and the 'if' keywords have been around for the better of some fifteen years, and if you were to go back further, the 'if' keyword has been around since the days of C, some twenty years or so.

How can you compare parts of the language when there are massive dissimilarities?