operator[] and different behaviour for reading and writing

M

Mark Stijnman

A while ago I posted a question about how to get operator[] behave
differently for reading and writing. I basically wanted to make a
vector that can be queried about whether it is modified recently or
not. My first idea, using the const and non-const versions of
operator[], was clearly not correct, as was pointed out. Julián Albo
suggested I could use proxies to do that. I've done some googling for
proxies (also in this group) and personally, I think this issue should
go into the FAQ. It seems to have come up quite a few times (sometimes
in slightly different contexts) and the subject is complex enough to
warrant an easily found description of possible solutions and caveats.
Anyway, I came up with this implementation of a vector that counts the
changes made:

#include <vector>

class ChangeCountingVector {
public:
// constructor:
ChangeCountingVector(const int n=1): itsVector(n), itsChangeCounter(0)

{}
// return size:
unsigned int size() const { return itsVector.size(); }
// return number of changes since initialization:
unsigned long changes() const { return itsChangeCounter; }

// Proxy subclass for index operator with different read/write
behaviour:
class Proxy {
public:
// convert to double operator:
operator double() const { return itsCCVec.get(itsIndex); }
// assign from double:
const Proxy& operator=(const double newVal) const {
itsCCVec.put(itsIndex, newVal);
return *this;
}
// assign from a different proxy (so a = b works).
// Can't rely on the compiler generated default here:
const Proxy& operator=(const Proxy& p) const {
itsCCVec.put(itsIndex, p.itsCCVec.get(p.itsIndex));
return *this;
}

private:
// Constructor is private to prevent instantiating Proxy
// objects by other classes:
Proxy(ChangeCountingVector& theVec, int theIndex):
itsCCVec(theVec), itsIndex(theIndex) {}

// Owning class should be friend to be the only one allowed to
// instantiate Proxy objects:
friend class ChangeCountingVector;

// operator& is private so address can't be taken of a proxy object:
double* operator&() const ;

// reference to the vector:
ChangeCountingVector& itsCCVec;
// the index represented by the proxy:
int itsIndex;
};

const double operator[](const unsigned index) const {
return get(index);
}
Proxy operator[](const unsigned index) {
return Proxy(*this,index);
}
double get(const int index) const {
return itsVector[index];
}
double put(const int index, const double newVal) {
itsVector[index] = newVal;
++itsChangeCounter;
}

private:
std::vector<double> itsVector;
unsigned long itsChangeCounter;
};

Some benchmarking showed that this is fairly efficient. I used a
reference to the main vector in the Proxy class, instead of a pointer,
since it seemed to be slightly more efficient. I suspect that the
compiler can do a few more optimizations in that case. When all
compiler optimizations were used, g++ produced code that was comparable
in performance to a naive implementation, i.e. incrementing the counter
in the non-const operator[], regardless of whether it was writing or
only reading. Of course, this naive implementation will give incorrect
results - on reading, the vector will report being changed, but as a
performance comparison it served well enough.

On a side note: Obviously, incrementing the change counter on accessing
every single element is not the most efficient. In reality one would
add methods/operators to manipulate the vector as a whole, and count
that as one update. That's more efficient for processor optimizations
as well. Also, the Proxy class could use +=, -=, *= and /= operators
too.

My question: in most of the posts that I have found on Google
newsgroups and other sources on the internet in general, I have not
seen the "const Proxy& operator=(const Proxy& p) const" operator, just
an assignment operator that takes a double as an argument (or a
reference to a template object). But without this operator, the
compiler reports that it can't use the default assignment constructor
because of the presence of a non-static reference member. Can someone
please confirm that the code for this class is correct and that I'm not
doing anything potentially harmful in my reference juggling? Any
improvements that should be made? Thanks in advance,

regards Mark
 
V

Victor Bazarov

Mark said:
A while ago I posted a question about how to get operator[] behave
differently for reading and writing. I basically wanted to make a
vector that can be queried about whether it is modified recently or
not. My first idea, using the const and non-const versions of
operator[], was clearly not correct, as was pointed out. Julián Albo
suggested I could use proxies to do that. I've done some googling for
proxies (also in this group) and personally, I think this issue should
go into the FAQ. It seems to have come up quite a few times (sometimes
in slightly different contexts) and the subject is complex enough to
warrant an easily found description of possible solutions and caveats.
Anyway, [...]

I have no opinions on your implementation or possible improvements, sorry,
but I have an opinion on a related topic you mentioned. I don't think
that this should go in the FAQ simply because this particular issue while
has come up several times in the past, is most certainly not *frequent*.
I am yet to encounter such a problem in my career. Perhaps if you could
explain what the use of your change counter is, I might see it differently
but at this point, I don't see it.

That said, you are definitely free to contact Marshall Cline and make your
suggestion to him about what section it would go to and in what form.

As to the

const Blah & operator=(const Blah &) const;

, think about it. How is the assignment going to work if the left-hand
side of it is constant?

V
 
M

Mark Stijnman

I thought I had replied already yesterday, but it seems something on
the internet ate it - or I did something wrong, which is just as
likely. If you have already read a post very similar to this one,
please consider this one not written.

Victor said:
Mark said:
A while ago I posted a question about how to get operator[] behave
differently for reading and writing. I basically wanted to make a
vector that can be queried about whether it is modified recently or
not. My first idea, using the const and non-const versions of
operator[], was clearly not correct, as was pointed out. Julián Albo
suggested I could use proxies to do that. I've done some googling for
proxies (also in this group) and personally, I think this issue should
go into the FAQ. It seems to have come up quite a few times (sometimes
in slightly different contexts) and the subject is complex enough to
warrant an easily found description of possible solutions and caveats.
Anyway, [...]

I have no opinions on your implementation or possible improvements, sorry,
but I have an opinion on a related topic you mentioned. I don't think
that this should go in the FAQ simply because this particular issue while
has come up several times in the past, is most certainly not *frequent*.
I am yet to encounter such a problem in my career. Perhaps if you could
explain what the use of your change counter is, I might see it differently
but at this point, I don't see it.

The change counter is just an example of a vector that can give
information to clients as to whether it has changed or not. It could
just as well have been a simple "dirty" flag, or even a "Notify" call
to one or more Observers. In my case, I want to perform interpolation
on a vector of data and I want to cache the interpolation data
structures. It takes O(N) operations to regenerate those data, so one
does not want to do that for every call to the interpolate function.
Only when the data has actually changed should the interpolation data
structures be regenerated.

There are also other cases where people have wanted to have different
behaviour depending on wheter operator[] was used for assigning or just
reading. I have seen several people who wanted a sparse vector class,
that only stores non-zero elements. Reading from a position where no
non-zero element is defined should return 0. Writing to such a position
should however insert a new non-zero element into the sparse vector.

Granted, you can always use 'get' and 'set' (or 'read' and 'write')
members, and a lot of people seem to think you always should, but a lot
of other people (including me) like the [] form, since it makes code
look more intuitive.
That said, you are definitely free to contact Marshall Cline and make your
suggestion to him about what section it would go to and in what form.

As to the

const Blah & operator=(const Blah &) const;

, think about it. How is the assignment going to work if the left-hand
side of it is constant?

V

The Proxy will not change, only the reference it represents changes. So
the Proxy object can be declared const. It looks counter-intuitive, but
it works.

regards Mark
 
V

Victor Bazarov

Mark said:
I thought I had replied already yesterday, but it seems something on
the internet ate it - or I did something wrong, which is just as
likely. If you have already read a post very similar to this one,
please consider this one not written.

I haven't.
[...]
The change counter is just an example of a vector that can give
information to clients as to whether it has changed or not. It could
just as well have been a simple "dirty" flag, or even a "Notify" call
to one or more Observers. In my case, I want to perform interpolation
on a vector of data and I want to cache the interpolation data
structures. It takes O(N) operations to regenerate those data, so one
does not want to do that for every call to the interpolate function.
Only when the data has actually changed should the interpolation data
structures be regenerated.

Yet another reason to make "dirty" flag to be settable from outside.
There are also other cases where people have wanted to have different
behaviour depending on wheter operator[] was used for assigning or just
reading. I have seen several people who wanted a sparse vector class,
that only stores non-zero elements. Reading from a position where no
non-zero element is defined should return 0. Writing to such a position
should however insert a new non-zero element into the sparse vector.

I am unable to come up with a solution (so far) when a bit more complex
mechanism is used than a simple assignment operator. See below.
Granted, you can always use 'get' and 'set' (or 'read' and 'write')
members, and a lot of people seem to think you always should, but a lot
of other people (including me) like the [] form, since it makes code
look more intuitive.

Really? *More* intuitive? Compare the behaviour of that operator for
std::map. Notice how behaviour of it is *not* different depending on
which side of the assignment operator it's on. *Not* different. And
it's not because it's impossible to implement. It's because it is
counter-intuitive, again.

Think about how you should implement this (and how much more intuitive it
is) when you have this situation:

void set_arg_to_three(double& arg) {
arg = 3;
}
...
yourspecialvector<double> vd(100);
set_arg_to_three(vd[99]); // I want vd[99] to now be 3

How is your proxy going to help you here?

Yes, the example *seems* contrived. Why don't I simply use

vd[99] = some_way_to_get_my_three_here();

notation, right? And, yes, if I could, I probably would. But if I am
stuck using a third-party library? I am then forced to write something
like

{ double temp;
set_arg_to_three(temp);
vd[99] = temp; }

or

inline double set_arg_to_three_adapter() // my new wrapper function
{
double temp;
set_arg_to_three(temp);
return temp;
}
...
vd[99] = set_arg_to_three_adapter();

both are really pushing the whole intuitiveness argument.
[...about operator= for a const object...]
The Proxy will not change, only the reference it represents changes. So
the Proxy object can be declared const. It looks counter-intuitive, but
it works.

Yet another argument not to do that. If it looks counter-intuitive, it
*is* counter-intuitive.

I am not saying "don't do that". I am just giving you the reasons why I
still think that it's not a *frequently* asked question and why before you
ever initiate the process of putting it in the FAQ, you should think hard
*what* you put in the FAQ as the answer for that question.

V
 
M

Mark Stijnman

Victor said:
[...]
The change counter is just an example of a vector that can give
information to clients as to whether it has changed or not. It could
just as well have been a simple "dirty" flag, or even a "Notify" call
to one or more Observers. In my case, I want to perform interpolation
on a vector of data and I want to cache the interpolation data
structures. It takes O(N) operations to regenerate those data, so one
does not want to do that for every call to the interpolate function.
Only when the data has actually changed should the interpolation data
structures be regenerated.

Yet another reason to make "dirty" flag to be settable from outside.

Sorry, I miss what reason you are referring to.
There are also other cases where people have wanted to have different
behaviour depending on wheter operator[] was used for assigning or just
reading. I have seen several people who wanted a sparse vector class,
that only stores non-zero elements. Reading from a position where no
non-zero element is defined should return 0. Writing to such a position
should however insert a new non-zero element into the sparse
vector.

I am unable to come up with a solution (so far) when a bit more complex
mechanism is used than a simple assignment operator. See below.
Granted, you can always use 'get' and 'set' (or 'read' and 'write')
members, and a lot of people seem to think you always should, but a lot
of other people (including me) like the [] form, since it makes code
look more intuitive.

Really? *More* intuitive? Compare the behaviour of that operator for
std::map. Notice how behaviour of it is *not* different depending on
which side of the assignment operator it's on. *Not* different. And
it's not because it's impossible to implement. It's because it is
counter-intuitive, again.

With 'intuitive' I mean that for a lot of objects like vectors, it's
intuitive to use index operators to access the elements. Of course the
implementation of the index operators themselves should be intuitive. I
would like to have an operator[] to not modify the visible state of the
object it is called on, when it is only used as a rvlaue. In this
respect, the operator[] on map is indeed not intuitive. When the
implementation is not intuitive, it should probably be avoided.
Think about how you should implement this (and how much more intuitive it
is) when you have this situation:

void set_arg_to_three(double& arg) {
arg = 3;
}
...
yourspecialvector<double> vd(100);
set_arg_to_three(vd[99]); // I want vd[99] to now be 3

How is your proxy going to help you here?

I see your point, and it is clearly a weakness of this approach. I also
would not know a way around this - it's solution would be highly
non-trivial.

Unfortunately, your example would also not work using the set and get
methods provided. Only a 'naked reference' to the data would work
there, after which the flag or change counter should be updated
manually. Unfortunately, that will break encapsulation, in that it
allows one to change the data, without updating the change counter.
[...about operator= for a const object...]
The Proxy will not change, only the reference it represents changes. So
the Proxy object can be declared const. It looks counter-intuitive, but
it works.

Yet another argument not to do that. If it looks counter-intuitive, it
*is* counter-intuitive.

I am not saying "don't do that". I am just giving you the reasons why I
still think that it's not a *frequently* asked question and why before you
ever initiate the process of putting it in the FAQ, you should think hard
*what* you put in the FAQ as the answer for that question.

V

I agree, which is why I put it up for discussion. And even a "you
can't" or "you can, but with such-and-such limitations" can already be
helpful as an answer to a FAQ.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,902
Latest member
Elena68X5

Latest Threads

Top