[comp.lang.c++] distinguishing operator[] on left and right

gilley@ndl.com (Greg Gilley) (03/01/91)

Is there any way to distinguish when the operator[] is used as an
lvalue as opposed to an rvalue?

The problem is that I am attempting to do a delayed copy of contents
with a reference count (a sort of copy-on-write).  So what I have is
a class of the form:

class X
{
    struct Xrep
    {
	float *data;
	int refcnt;
	int length;
    } *p;
    .
    .
    .
};

and operator= looks like:
void X::operator=(const X &x)
{
    x.p->refcnt++;

    if (--p->refcnt == 0)
    {
	delete p->data;
	delete p;
    }

    p = x.p;
}

Which is all well and good.  If I do other operators, they create new
ones, etc.  The problem arises with [].  Suppose we have:

     X a(1);
     X b(1);

     a[0] = 1.0;
     b = a;
     b[0] = 10.0;

What you would like to have is a[0] == 1.0 and b[0] == 10.0 (which means
that b has to be "detached" from a before it changes).  However,
I can't find a way to distinguish when the result of [] is being used
as an lvalue or rvalue.

Any suggestions?  Thanks,

			Greg

-- 
-------------------------------------------------------
  Greg Gilley
  gilley@ndl.COM   [Numerical Design Limited]
  919-929-2917 (voice)

jbuck@galileo.berkeley.edu (Joe Buck) (03/02/91)

In article <1991Feb28.212419.20920@ndl.com>, gilley@ndl.com (Greg Gilley) writes:
|> Is there any way to distinguish when the operator[] is used as an
|> lvalue as opposed to an rvalue?

Yes, something like this:

class ElementRef {
	operator int ();
	operator=(int);
};

class IntVector {
...
	ElementRef operator[] (int);
};

The idea is that [] returns a special class called ElementRef, which
has two operators defined: assignment operator and cast-to-int.
If I say

	IntVector v;
	int i, j;
	...
	v[3] = i;
	j = v[4];

in the first case, ElementRef::operator=(int) is used.  In the
second case, ElementRef::operator int() is used.

--
Joe Buck
jbuck@galileo.berkeley.edu	 {uunet,ucbvax}!galileo.berkeley.edu!jbuck	

horstman@mathcs.sjsu.edu (Cay Horstmann) (03/02/91)

In article <1991Feb28.212419.20920@ndl.com> gilley@ndl.com (Greg Gilley) writes:
>Is there any way to distinguish when the operator[] is used as an
>lvalue as opposed to an rvalue?
>
Surely I am not the first one to propose this one, but here goes anyway.

Just as pre- and postincrement operator++ are distinguished by a hidden
int argument, could this not be done for lvalue and rvalue operator[]?

I.e. const X& operator[]( int ) and X& operator[]( int, int )?

The second int is always 0. 

If the second operator is not present, the first one is taken for both
lvalues and rvalues. 

Points to consider:
   (1) It is ugly as hell
   (2) It has precedent (operator++)
   (3) It won't break existing code
   (4) It does not use the keyword "static" in unusual ways.

Cay

wmm@world.std.com (William M Miller) (03/03/91)

horstman@mathcs.sjsu.edu (Cay Horstmann) writes:
> Just as pre- and postincrement operator++ are distinguished by a hidden
> int argument, could this not be done for lvalue and rvalue operator[]?
>
> I.e. const X& operator[]( int ) and X& operator[]( int, int )?
>
> The second int is always 0.
>
> If the second operator is not present, the first one is taken for both
> lvalues and rvalues.
>
> Points to consider:
>    (1) It is ugly as hell

No argument here. :-)  Seriously, I don't like the operator++() and
operator--() solution in E&S, either, although I don't know whether it's
worth fighting to change.

>    (2) It has precedent (operator++)
>    (3) It won't break existing code

Here's where I think the real problem with this proposal lies: it really
isn't parallel to the operator++() design.  The operator++() design *does*
break existing code -- if you don't provide the two-argument form, you can't
use postfix ++.  It would be YACC (Yet Another C++ Complexity :-) for
operator[]() to fold the cases but for operator++() not to do so.

The proposal also suffers from lack of generality: operator[]() is not the
only operator that can be used in lvalue and rvalue contexts.  Even if we
restrict ourselves to operators that can have lvalue results on builtin
types, there are (prefix) ++, (prefix) --, "," (comma), and all the
assignment operators (not to mention ?:, since it can't be overloaded); more
generally, all the overloaded operators can return a reference and hence be
used in lvalue contexts.  There's no compelling reason to believe that only
operator[]() would benefit from being able to distinguish between lvalue and
rvalue contexts.  For instance, how would you extend this to apply to prefix
operator++()?  There's already a meaning for operator++(int).

Another major consideration X3J16 applies to proposals is if there is a
straightforward way to achieve the desired results in the existing language.
As someone pointed out in an earlier posting, making operator[]() return an
object of an auxiliary class with separate operator=() and conversion
operator member functions is a pretty reasonable way to address this problem
where it's needed, and it's more generally applicable, as well.

-- William M. Miller, Glockenspiel, Ltd.
   wmm@world.std.com

jbuck@galileo.berkeley.edu (Joe Buck) (03/04/91)

In article <1991Mar2.000705.3496@mathcs.sjsu.edu> horstman@mathcs.sjsu.edu (Cay Horstmann) writes:
>In article <1991Feb28.212419.20920@ndl.com> gilley@ndl.com (Greg Gilley) writes:
>>Is there any way to distinguish when the operator[] is used as an
>>lvalue as opposed to an rvalue?
>>
>Surely I am not the first one to propose this one, but here goes anyway.
>
>Just as pre- and postincrement operator++ are distinguished by a hidden
>int argument, could this not be done for lvalue and rvalue operator[]?

The difference is that it was needed for ++; there is otherwise no way to
tell the difference between predecrement and postdecrement, so you couldn't
make smart pointer classes look like pointers.

However, it's not difficult at all to get the proper behavior from the
existing language with operator[].  If you want to get a different
operation when the result of operator[] is used as an rvalue than you
do when it is used as an lvalue, you can have operator[] return a special
class that has both an assignment operator (which is used in the lvalue
case) and a cast operator (which is used in the rvalue case).

>Points to consider:
>   (1) It is ugly as hell
>   (2) It has precedent (operator++)

I disagree that this is a precedent.  There was no way before to tell
++p from p++ where p is a class.  It's not difficult at all to handle
v[key] = x, and x = v[key] correctly where v[key] is, say, a hash table and
key is a key, and the entry for key may or may not exist.  I take it
this is a case where you think there is a problem that needs to be
solved by a language extension.  But there is no problem, and the code
to do it right is less ugly than your method.

>   (3) It won't break existing code
>   (4) It does not use the keyword "static" in unusual ways.

    (5) It is completely unnecessary
    (6) It will lead to people making other unnecessary "extensions"
because there are other analogous situations that people will think need
"solutions"

--
--
Joe Buck
jbuck@galileo.berkeley.edu	 {uunet,ucbvax}!galileo.berkeley.edu!jbuck	

jar@ifi.uio.no (Jo Are Rosland) (03/04/91)

In article <1991Mar2.000705.3496@mathcs.sjsu.edu> horstman@mathcs.sjsu.edu (Cay Horstmann) writes:
   Surely I am not the first one to propose this one, but here goes anyway.

   Just as pre- and postincrement operator++ are distinguished by a hidden
   int argument, could this not be done for lvalue and rvalue operator[]?

As someone else already pointed out, the lvalue/rvalue distinction is
already achievable within the current language, through the use of an
intermediate value returned from operator[].

The problem with this, is optimization.  You probably want something
like operator[] to be as fast as possible, and to do this in C++
today, you have to create an interface based on separate, inlined,
get/set operations.

A related example of something that's achievable, but not efficient,
is a string concatenation operator.  An interface like:

	String s1 = "foo";
	String s2 = "bar";
	String s3;

	s3 = s1 + s2;

would probably be a nice way to handle string concatenation, but this
can't be done efficiently.  Instead you'll probably have to do
something like:

	String s1 = "foo";
	String s2 = "bar";
	String s3;

	s3 = s1;
	s3 += s2;

To me, this use of intermediate values -- both compiler generated and
as part of class interface implementations -- is a serious problem
with C++, due to the performance degradation it leads to.

I mean, a very popular first project (and perhaps second and third
:-)) after having learned C++, is to create some kind of string class.
But how many of those string classes are actually in use?  After one
realizes one have to choose between a significant performance hit, or
a counterintuitive interface, I think many programmers go back to the
traditional strdup/strcpy/strcat way of handling strings.

It's not really that much to gain by renaming these as operator=,
operator+= and so on, since you (and the maintainers of your code)
would have to look up the implementations of these operators to make
sure there are no hidden surprises concerning things like copying.

Which brings me to another problem with C++, and probably the whole
OOP paradigm.  We're badly in need of some way of precisely specifying
interfaces to modules/classes.  This specification should include
performance of methods, as well as all the interesting parts of their
behaviour (sp?).

In practice, this should be something halfway between a C++ class
header file, and its implementation.  Specifications should be
standardized and powerfull enough to allow browse/search tools that
can aid in finding classes that match criteria like language, a set of
operations needed, and performance.

Only after this is achieved, can we hope to meet the "software ic"
goal of OOP, including things like interchangable software modules.
--
Jo Are Rosland
jar@ifi.uio.no

robert@am.dsir.govt.nz (Robert Davies) (03/05/91)

re: distinguishing operator[] on the left and right.

This follows up Greg Gilley's item. I think C++ could be improved here.
The "right" way of distinguishing the access to g in

      char c = g[3];

and

      g[3] = c;

would be with "const".

You need two versions of the subscript function (my examples are based on Tony
Hansen's string class):


      char &operator[](int i)
      {
         if (p->refcount > 1) disconnect();
         return str()[i];
      }

      char operator[](int i) const
      {
         return str()[i];
      }

Currently (in Turbo C++ or Glockenspiel C++; my version of Zortech can't
distinguish between the 2 versions) you need to write

      char c = ((const string)g)[3];

to get the second version. Which is a bit messy.

But wouldn't it be reasonable for the second version to be the default if the
compiler can tell that the operation won't affect the value of g?  Was this
kind of issue raised in the recent discussion on const in comp.std.c++?

I don't think Joe Buck's solution using an extra class is fully satisfactory
as it uses up the one coercion that C++ allows. For example, in his example,

      double d = v[4];

will not work.


Finally, do we really need delayed copy - or does it just cause more trouble
than it is worth? The only place it might be necessary is in returning values
from a function. And someone suggested that under some circumstances a clever 
compiler could avoid the copy you would ordinarily expect in a return.

Robert

fuchs@tmipe0.telematik.informatik.uni-karlsruhe.de (Harald Fuchs) (03/05/91)

robert@am.dsir.govt.nz (Robert Davies) writes:

>Currently (in Turbo C++ or Glockenspiel C++; my version of Zortech can't
>distinguish between the 2 versions) you need to write

>      char c = ((const string)g)[3];

>to get the second version. Which is a bit messy.

>But wouldn't it be reasonable for the second version to be the default if the
>compiler can tell that the operation won't affect the value of g?

Won't work. In general, the only way for a compiler to know about
constness is just by declaring a member function const. A non-const
operator[] is legal and sometimes even reasonable. Const vs. non-const
and LHS vs. RHS are completely different matters.
--

Harald Fuchs <fuchs@telematik.informatik.uni-karlsruhe.de>

stephens@motcid.UUCP (Kurt Stephens) (03/06/91)

robert@am.dsir.govt.nz (Robert Davies) writes:
>Currently (in Turbo C++ or Glockenspiel C++; my version of Zortech can't
>distinguish between the 2 versions) you need to write

>      char c = ((const string)g)[3];

>to get the second version. Which is a bit messy.

>But wouldn't it be reasonable for the second version to be the default if the
>compiler can tell that the operation won't affect the value of g?  Was this
>kind of issue raised in the recent discussion on const in comp.std.c++?

	How could the compiler tell that the operation won't affect the
value of g? the string::operator[]() could be doing just about anything
to the privates of g.  C++ compliers cannot read minds,
or understand the internal semantics of any functions.

Example:
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-==-=-=-=-=-=-=-=-=-
#include	<stdio.h>

class X {
	int	i;
public:
	X( int I ) : i(I) {}
	int	operator[](int I) { return i = I; }
	void	print() { printf("%d\n", i ); }
};

main() {
	X	a = 1;
	a.print();
	a[-1];
	a.print();
}
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

X::operator[](int) means "assign to member i", but because of the
association with C's operator[], most container classes limit operator[]()
semantics to be "return a lvalue", "return a rvalue" or "lookup in table",
which is great because it makes for readable code.

Obviously, X::operator[](int) is a bad choice for "assign to member i";
X::operator=(int) would have been much more intuitive handle.

But we (and the complier) cannot assume that operator[]() never
modifies the state of the class or its instances.

Kurt A. Stephens		Foo::Foo(){return Foo();}
stephens@void.rtsg.mot.com	"When in doubt, recurse."

-- 

Kurt A. Stephens		Foo::Foo(){return Foo();}
stephens@void.rtsg.mot.com	"When in doubt, recurse."

chip@tct.uucp (Chip Salzenberg) (03/06/91)

According to jar@ifi.uio.no (Jo Are Rosland):
>I mean, a very popular first project (and perhaps second and third
>:-)) after having learned C++, is to create some kind of string class.
>But how many of those string classes are actually in use?

We still use ours.  The "a = b; a += c" syntax is a little awkward,
but it's sure better than calculating sizes, calling new/delete and
strcpy/strcat.

Note, though, that a |String| is often implicitly converted to a
|const char *| when used as a function parameter, since functions that
expect strings may well be called with old-style C strings, and I
don't want to construct a temporary |String| in such cases.
-- 
Chip Salzenberg at Teltronics/TCT     <chip@tct.uucp>, <uunet!pdn!tct!chip>
   "All this is conjecture of course, since I *only* post in the nude.
    Nothing comes between me and my t.b.  Nothing."   -- Bill Coderre

ssd@engr.ucf.edu (Steven S. Dick) (03/07/91)

What if I have a [] operator that does something unusual to extract the
data from the object... for instance, a bitfield...

// interface parts only...
class packedbits 
{
 public:
  packedbits(int size);
  int operator[](int index);
  void set(int index);
  void clear(int index);
};

doit() 
{
   packetbits flags(100);

   if (flags[4])	// this works
     ....

   flags.set(4);	// works--but ugly
   flags[4] = 1;	// how can I make this work???
}

	Steve
ssd@engr.ucf.edu

robert@am.dsir.govt.nz (Robert Davies) (03/07/91)

Distinguishing lvalues and rvalues and delayed copy.

I posted a note suggesting that Greg Gilley's problem could be resolved if
C++ handled constant member functions slightly differently.

The problem is to decide whether you need to do a copy when you access an
element of a string or vector in a delayed copy situation. See for example the
string class in Tony Hansen's book.

A couple of people replied. The answers didn't make a lot of sense so I wonder
if my item got damaged in transmission. I repeat the problem posed by Greg
Gilley. If you have a string class (for example) with delayed copy how do you
tell it to copy, if necessary, when you have a statement like

   String g = f;
   ....
   g[3] = 'a';

so that g gets changed but not f (both get changed if you use the code in Tony
Hansen's book). But you don't want to copy when you have

   String g = f;
   ....
   char c = g[3];


I suggested having two versions of the operator[]

      char &operator[](int i)
      {
         if (p->refcount > 1) disconnect();   // do the delayed copy now
         return str()[i];                     // get the ref to the element
      }

      char operator[](int i) const            // constant member function
                                              // ARM 9.3.1
      {
         return str()[i];                     // get the element
      }


Currently (in Turbo C++ or Sun C++) you need to write 

      char c = ((const String)g)[3];

to get the second version. Which is a bit messy. And the Sun version makes an
extra copy of g.

I suggest that it would be reasonable for the second version to be the
default if the compiler can tell that the operation won't affect the value of
g.

The line

   char c = g[3];

will not affect g and the compiler knows this, since in this case the = is
predefined. If it is a user defined = then it will depend on whether the = has
its argument declared const. Assume it has.

Now suppose there are the two versions of operator[] defined in class String:

   char& operator[](int);

and

   char operator[](int) const;

The second version is guaranteed not to affect g. So either version of
operator[] is OK. The compilers I have access to choose the first version
in c = g[3] (unless g is declared const). They could equally well have used
the second version. In other words there would be no damage if the compiler
had decided that g had been declared constant.

So I suggest, if there are both a const member function and an ordinary member
function defined, then the compiler should pretend that the object (g in our
case) has been declared "const" and choose the const member version of the
function if this compiles OK.

This will ensure that the statement

   char c = g[3];

will get the version of operator[] that doesn't cause a copy.

On the other hand

   g[3] = 'a';

is not allowed if g is const so the compiler must choose the ordinary member
function version of operator [].

jimad@microsoft.UUCP (Jim ADCOCK) (03/08/91)

In article <1991Mar2.212017.13885@world.std.com> wmm@world.std.com (William M Miller) writes:
|Another major consideration X3J16 applies to proposals is if there is a
|straightforward way to achieve the desired results in the existing language.
|As someone pointed out in an earlier posting, making operator[]() return an
|object of an auxiliary class with separate operator=() and conversion
|operator member functions is a pretty reasonable way to address this problem
|where it's needed, and it's more generally applicable, as well.

.... assuming that the committee accepts overloaded operator dot.  Otherwise,
as Cay has pointed out, making operator[] return an auxiliary class
[ dare we call it a "reference" class ??? ] is not a general solution,
since it cannot be dereferenced like a normal object.

chip@tct.uucp (Chip Salzenberg) (03/09/91)

According to robert@am.dsir.govt.nz (Robert Davies):
>I suggested having two versions of the operator[]
>      char &operator[](int i)
>      char operator[](int i) const

I've done this.

>      char c = ((const String)g)[3];

Perhaps you meant

       char c = ((const String &)g)[3];

That's less likely to create a temporary.

>I suggest that it would be reasonable for the second version to be the
>default if the compiler can tell that the operation won't affect the value
>of g.

There's the rub.  How's the compiler supposed to know that?  We may
know from reading the class definition what's meant, but the compiler
hasn't got a prayer at figuring out when to call the const function
even though you're not operating on a const object.

A workaround would be to create a const reference to the object in
question, and use it for access:

     String s;
     String &r = s;
     char c = s[0];    // slow
     char d = r[0];    // fast

-- 
Chip Salzenberg at Teltronics/TCT     <chip@tct.uucp>, <uunet!pdn!tct!chip>
 "Most of my code is written by myself.  That is why so little gets done."
                 -- Herman "HLLs will never fly" Rubin

beng@microsoft.UUCP (Ben GOETTER) (03/09/91)

In article <27D3E61F.6226@tct.uucp> chip@tct.uucp (Chip Salzenberg) writes:
| According to jar@ifi.uio.no (Jo Are Rosland):
| >I mean, a very popular first project (and perhaps second and third
| >:-)) after having learned C++, is to create some kind of string class.
| >But how many of those string classes are actually in use?
| 
| We still use ours.

As do we.  For us, the big win lies less in automatic storage management than
it does in localization of double-byte character-set (DBCS) dependencies.
It has also in the past handled conversion from application resourcefiles
and to/from alien string formats (e.g. non-ASCIZ).

Unfortunately, an airtight set of DBCS-safe methods renders impossible the
traditional byte-vector string manipulation so beloved by C hacks.  The
usual ad-hoc lexing becomes very awkward.  Make sure you have an adequate
replacement for char-at-a-time tokenizing and pattern-matching, lest you
be lynched by your angry clients.  (I still sport rope burns....)

--
Ben Goetter, microsoft!beng

markt@nro.cs.athabascau.ca (Mark Tarrabain) (03/10/91)

ssd@engr.ucf.edu (Steven S. Dick) writes:

> What if I have a [] operator that does something unusual to extract the
> data from the object... for instance, a bitfield...
> 
> // interface parts only...
> class packedbits 
> {
>  public:
>   packedbits(int size);
>   int operator[](int index);
>   void set(int index);
>   void clear(int index);
> };
> 
> doit() 
> {
>    packetbits flags(100);
> 
>    if (flags[4])	// this works
>      ....
> 
>    flags.set(4);	// works--but ugly
>    flags[4] = 1;	// how can I make this work???
> }
> 
> 	Steve
> ssd@engr.ucf.edu

The line reading:
        int operator[](int index);
should be:
        int &operator[](int index);
then it will work on the left or the right side of an =.

>> Mark

jbuck@galileo.berkeley.edu (Joe Buck) (03/12/91)

In article <1991Mar6.235058.3641@osceola.cs.ucf.edu>, ssd@engr.ucf.edu (Steven S. Dick) writes:
|> What if I have a [] operator that does something unusual to extract the
|> data from the object... for instance, a bitfield...
|> 
|> // interface parts only...
|> class packedbits 
|> {
|>  public:
|>   packedbits(int size);
|>   int operator[](int index);
|>   void set(int index);
|>   void clear(int index);
|> };

Your problem is right there.  You're having operator[] return an int.  This
means that it can only be used as an lvalue and cannot be used to set the
bit.

Let's send a helper class to the rescue: change operator[](int) to return
a BitRef helper class:

class BitRef {
private:
	packedbits& pb;
	int index;
public:
	BitRef (packedbits& obj, int idx) : pb(obj), index(idx) {}
	operator int() { return pb.readBit(index);}
	BitRef& operator=(int newBit) {
		if (newBit) pb.set(index);
		else pb.clear(index);
		return *this;
	}
};

I need a new function in class packedbits: readBit(int) returns
the value of the bit at the given position.

Now when I say

	packedbits bitarray;

	int x = bitarray[23];

this turns into x = bitarray.readBit(23);

and

	bitarray[34] = x;

turns into

	if (x) bitarray.set(34); else bitarray.clear(34);

Note that the returned object acts like a reference to the
given bit.  In the case where the thing returned is an object,
we'd like to be able to redefine operator dot (to have a "smart
reference" class).  We can't with the ARM, though smart references
have been proposed as an extension to the language.

--
Joe Buck
jbuck@galileo.berkeley.edu	 {uunet,ucbvax}!galileo.berkeley.edu!jbuck