[comp.lang.c++] operator []: ref vs value

dougm@zamenhof.rice.edu (Doug Moore) (02/23/90)

I can't seem to find a way to do precisely what I want; I want a
sparse vector data type.  I can declare a float-returning operator []
that returns 0.0 if the array index corresponds to a row for which no
nonzero is stored.  I can declare a reference-to-float-returning
operator[] that creates a new entry in my linked list of nonzero
values when the row has no corresponding nonzero stored.
Unfortunately, I want one behavior when the pseudo-array access occurs
as an rvalue and another when it appears as an lvalue.  Can I get what
I want?

Dougm

daniel@saturn.ucsc.edu (Daniel Edelson) (02/23/90)

In article <5216@brazos.Rice.edu> dougm@zamenhof.rice.edu (Doug Moore) writes:
>
>I can't seem to find a way to do precisely what I want; I want a
>sparse vector data type.  I can declare a float-returning operator []
>that returns 0.0 if the array index corresponds to a row for which no
>nonzero is stored.  I can declare a reference-to-float-returning
>operator[] that creates a new entry in my linked list of nonzero
>values when the row has no corresponding nonzero stored.
>Unfortunately, I want one behavior when the pseudo-array access occurs
>as an rvalue and another when it appears as an lvalue.  Can I get what
>I want?
>
>Dougm

You can have the [] operator return a dummy type and then overload
the assignment operator on the dummy type. This is not real efficient
but it seems to give correct semantics. I wrote this code for reference
counting vectors efficiently, but its not efficient. It should work for
sparse vectors well, though.

class vector;

struct dummy {		// A dummy refers to a specific vector element
	vector * vp;
	int index;
	operator int&()			{ /* return the element value */} 
	void operator=(int & t);
	dummy(vector *vvpp, int i) : vp(vvpp), index(i) { }
};

struct vec {
	// implementation details are yours

	dummy & operator[](int index) { return dummy(this, index); }
};

inline
void dummy::operator=(int & t) 
{ 
	// Do whatever necessary to assign t to the vector 
	// component at index in *vp.
}

daniel@cis.ucsc.edu
daniel edelson

jeffa@hpmwtd.HP.COM (Jeff Aguilera) (02/24/90)

> I can't seem to find a way to do precisely what I want; I want a
> sparse vector data type.  I can declare a float-returning operator []
> that returns 0.0 if the array index corresponds to a row for which no
> nonzero is stored.  I can declare a reference-to-float-returning
> operator[] that creates a new entry in my linked list of nonzero
> values when the row has no corresponding nonzero stored.
> Unfortunately, I want one behavior when the pseudo-array access occurs
> as an rvalue and another when it appears as an lvalue.  Can I get what
> I want?

Here's one way, but it's not so pretty:  

	float spare_matrix::operator[](int);	//read-only access
	float& spare_matrix::operator()(int);	//possible write-to access

-----
jeffa

chip@tct.uucp (Chip Salzenberg) (02/24/90)

According to dougm@zamenhof.rice.edu (Doug Moore):
>Unfortunately, I want one behavior when the pseudo-array access occurs
>as an rvalue and another when it appears as an lvalue.  Can I get what
>I want?

You almost had it.  Define a reference_to_missing_array_element type,
and have operator[] return an object of that type.  Then define:

	reference_to_missing_array_element::operator float()

		return 0.0

	reference_to_missing_array_element::operator = (float)

		create the missing array element and assign to it

No sweat.

dl@g.g.oswego.edu (Doug Lea) (02/25/90)

> From: chip@tct.uucp (Chip Salzenberg):
> 
> According to dougm@zamenhof.rice.edu (Doug Moore):
> >Unfortunately, I want one behavior when the pseudo-array access occurs
> >as an rvalue and another when it appears as an lvalue.  Can I get what
> >I want?
> 
> You almost had it.  Define a reference_to_missing_array_element type,
> and have operator[] return an object of that type.  Then define:
> 
> 	reference_to_missing_array_element::operator float()
> 
> 		return 0.0
> 
> 	reference_to_missing_array_element::operator = (float)
> 
> 		create the missing array element and assign to it
> 
> No sweat.
> 

Consider what happens in

void inc(float& f) { f += 1.0; }

main()
{
   // ...
  inc(v[0]);
}

Here, v[0] returns a reference_to_missing_array_element, which is
`coerced' to return a constant value (0.0). The 0.0 is made into a
temporary variable in order to make a reference. This temporary is
then modified in inc(), without touching the underlying representation
in v. This is probably not what a user would have in mind. 

-Doug
--
Doug Lea, Computer Science Dept., SUNY Oswego, Oswego, NY, 13126 (315)341-2367
email: dl@oswego.edu              or dl%oswego.edu@nisc.nyser.net
UUCP :...cornell!devvax!oswego!dl or ...rutgers!sunybcs!oswego!dl

roger@procase.UUCP (Roger H. Scott) (02/27/90)

In article <5216@brazos.Rice.edu> dougm@zamenhof.rice.edu (Doug Moore) writes:
>
>I can't seem to find a way to do precisely what I want; I want a
>sparse vector data type.  I can declare a float-returning operator []
>that returns 0.0 if the array index corresponds to a row for which no
>nonzero is stored.  I can declare a reference-to-float-returning
>operator[] that creates a new entry in my linked list of nonzero
>values when the row has no corresponding nonzero stored.
>Unfortunately, I want one behavior when the pseudo-array access occurs
>as an rvalue and another when it appears as an lvalue.  Can I get what
>I want?

I'm so glad you asked about this!  This is one of my favorite C++ inventions.

typedef unsigned Index;
class SomeType;

class SparseVector {
public:
    SparseVector();
    SparseVector(unsigned size);
    ~SparseVector();
    ...

    SparseVectorRef operator[](Index);

private:
    ... // guts
    friend class SparseVectorRef;
    void storeAt(Index, SomeType); // implementation of store
    SomeType at(Index); // implementation of retrieval
};

class SparseVectorRef {
public:
    operator SomeType() {return v->at(i);}
    void operator=(SomeType val) {v->storeAt(i, val);}
    // other functions, such as operator&(), can be added if you want to
    // emulate C arrays (for whatever twisted reason)

private:
    friend class SparseVector;
    SparseVectorRef(SparseVector *vv, Index ii) : v(vv), i(ii) {}
    SparseVector *v;
    Index i;
};

inline
SparseVectorRef
SparseVector::operator[](Index i) {
    return SparseVectorRef(this, i);
}

void
example() {
    SparseVector v10(10);
    SomeType a, b;

    v10[3] = a; // v10.operator[](3).operator=(a);
    b = v10[7]; // b = v10.operator[](7).operator SomeType();
}

Now, wasn't that obvious? ;-]

jimad@microsoft.UUCP (Jim ADCOCK) (03/01/90)

/****
You can't make two versions of op[] for lvalue and rvalue, but you can
differentiate the situations where an op[] is taken, and where an op[] is
taken and value is changed.  The general trick is for op[] to return a 
smart proxy for that element, and a special assignment to the proxy is
defined that updates the vector if the value of the proxy, [and thus the
element] changes.  Just to confuse things, in the below sketchy example,
I use the same class "SVEL" for both the elements of the vector, and the
proxy to that element.
****/

class SVEL// a sparse vector element
{
	SVEL* psvel;
	class SV& sv; 
	int i;
	double d;
public:
	SVEL(SV& svT, int iT, SVEL* psvelT, double dT=0.0);
	SVEL(SV& svT, int iT);
	SVEL& operator=(double d);
	void Attach(SVEL* psvelT) { psvel = psvelT; }
	SVEL* Next() { return psvel; }
	int Index() { return i; }
	void Print();
	operator double() { return d; }
};
SVEL::SVEL(SV& svT, int iT, SVEL* psvelT, double dT) : 
	sv(svT), i(iT), psvel(psvelT), d(dT) {}

SVEL::SVEL(SV& svT, int iT) : 
	sv(svT), i(iT), psvel(0), d(0) {}

void SVEL::Print() { printf("(%d, ",i); printf("%g) ",d); }

class SV	// a sparse vector
{
	SVEL* psvel;
public:
	SV();
	void Attach(SVEL* psvelT) 
	{ SVEL* pT = psvel; psvel = psvelT; psvel->Attach(pT); }
	SVEL& operator[](int i);
	void Print();
};
SV::SV() : psvel(0) {}

SVEL& SVEL::operator=(double dT)
{
	if ((d==0.0) && (dT!=0.0))
	{
		d = dT;
		sv.Attach(this);
	}
	else d = dT;

	return *this;
}

void SV::Print()
{
	SVEL* p = psvel;
	while (p)
	{
		p->Print();
		p = p->Next();
	}
	putchar('\n');
}

SVEL& SV::operator[](int i)
{
	SVEL* p = psvel;
	while (p && (i != p->Index()))
		p = p->Next();

	if (p)
		return *p;
	else
		return *new SVEL(*this, i);
}

int main()
{
	SV v1, v2;
	v1[100] = 10.0;
	v1[200] = 20.0;
	v2[10] = v1[1];
	v2[20] = v1[200];
	v2[30] = 300.0;
	v1.Print();
	v2.Print();
	v2[20] = 200.0;
	v2.Print();
	double d20 = v2[20];
	printf("%g\n", d20);

	return 0;
}

// gc left as an exercise :-)

jimad@microsoft.UUCP (Jim ADCOCK) (03/06/90)

In article <DL.90Feb25100053@g.g.oswego.edu> dl@oswego.edu writes:
XConsider what happens in
X
Xvoid inc(float& f) { f += 1.0; }
X
Xmain()
X{
X   // ...
X  inc(v[0]);
X}
X
XHere, v[0] returns a reference_to_missing_array_element, which is
X`coerced' to return a constant value (0.0). The 0.0 is made into a
Xtemporary variable in order to make a reference. This temporary is
Xthen modified in inc(), without touching the underlying representation
Xin v. This is probably not what a user would have in mind. 

The very latest specs on the _language_ call for a very different behavior
in this regard.  Namely only if the reference is to a const is a temporary
object created.  This prevents the situation mentioned where a hidden
temporary is created, modified, and then ignored -- all done behind the
users back.  Instead, this scenerio will give an error.  Naturally, it
will take an indeterminate amount of time for these language changes to
filter down to a particular compiler release.

Still, if one is designing a fundamental type like a sparse vector, it
would be nice if it works like a standard "C" vector.