Sign in to follow this  
eq

operator[]

Recommended Posts

Hi all, I'm trying to "detect" whenever a user writes to a value returned by the [] operator. Here's a small sample:
template <class T, class C> class Proxy
{
public:
	Proxy& operator=(const T& value)
	{
		m_t = value;
		m_c->updated(m_i);
		return this[0];
	}
	Proxy& operator=(const Proxy& p)
	{
		m_t = p.m_t;
		m_c->updated(m_i);
		return this[0];
	}
	Proxy(C* const c, T& t, const unsigned int i) : m_c(c), m_t(t), m_i(i)
	{
	}
	Proxy(const Proxy& p) : m_c(p.m_c), m_t(t), m_i(p.m_i)
	{
	}
	operator T()
	{
		return m_t;
	}
private:
	unsigned int m_i;
	C* m_c;
	T& m_t;
};

template <class T> class MyVec
{
public:
	MyVec(const std::vector<T>& v) :m_v(v)
	{
	}
	const T& operator[](const unsigned int i) const
	{
		return m_v[i];
	}
	typedef Proxy<T, MyVec<T> > P;
	P operator[](const unsigned int i)
	{
		return P(this, m_v[i], i);
	}
private:
	friend class P;
	void updated(const unsigned int i)
	{
		OutputDebugString("Stuff written\n");
	}
	std::vector<T> m_v;
};

int
main(const unsigned int argCount, const char* const args[])
{
//	Create and init a vector that is used to set my vector
	std::vector<int> temp;
	temp.push_back(1);
	temp.push_back(2);
	temp.push_back(3);
//	Create test object
	MyVec<int> test(temp);
	int i = test[2];   // Returns 3 as expected
	test[1] = 10;      // Calls the updated method as expected
	test[1] = test[2]; // Calls the updated method as expected
	return 0;
}

Soo far so good, every time you write to test.m_v it the updated method is called (hence any changes is detected). Now let's start to cause problems (my favorite!) by using a more complex data type.
// Add
struct Foo
{
	Foo()
	{
	}
	Foo(const int bar, const int a) : m_bar(bar), m_a(a)
	{
	}
	int m_bar;
	int m_a;
};

// Replace
int
main(const unsigned int argCount, const char* const args[])
{
//	Create and init a vector that is used to set my vector
	std::vector<Foo> temp;
	temp.push_back(Foo(1, 0));
	temp.push_back(Foo(2, 1));
	temp.push_back(Foo(3, 2));
//	Create test object
	MyVec<Foo> test(temp);
	Foo i = test[2];       // Works, no problem here
	test[1] = Foo(10, 11); // Still works, calls the updated method
	test[1] = test[2];     // Same as above

	int fooBar = test[2].m_bar; // Don't compile! Obvious since test[2] is of type Proxy, not Foo as the syntax indicates.
	return 0;
}

So the last statement didn't compile and there's no way to overload the . operator in C++ (or is it?). What to do? Let's break intuitive syntax a bit and say: hey we don't allow . let's use -> instead:
//	Add to the public section of class Proxy
	const T* operator->()
	{
		return &m_t;
	}

//	Replace in main:
// 	int fooBar = test[2].m_bar;
// 	With:
	int fooBar = test[2]->m_bar; // Now we can read from the Foo object as expected

We've now added support to read from the object, but how can I go about allowing changes?
//	Add to main
	test[2]->m_bar = 77; // Doesn't compile since -> returns a const pointer, if we return a non-const instead, the assignment will compilebut then the change isn't detected, i.e updated is not called.

Is it possible to do something like this in C++, with a somewhat intuitive syntax, pherhaps using some sort of a recursive proxy? Any ideas?

Share this post


Link to post
Share on other sites
In general, no. With some constraints, as you discovered, yes.

However, why do you want to? It is counterintuitive to abuse the meaning of the indexing operator in that way (which is idiomatically not a "smart" method of indexing). Provide a member function with an appropriate name to do any kind of intelligent indexing and/or updating, it will make your interface signifigantly cleaner and more robust.

The implicit conversion operators tend to offer up a similar problem; by taking huge syntactic shortcuts, usually to save a little bit of typing, they embrittle the usability of the class in question.

Be explicit.

Share this post


Link to post
Share on other sites
Quote:
However, why do you want to?

I'd like to mimic stl containers because I'd like to be able to use all algorithm's etc (and also to provide a well known interface).

It actually works quite ok as is, i.e only allow writes to the whole object: test[2] = Foo(10, 10); but provide reading using the -> operator.
I have no problem running any of the algorithms that I've tried with my current implementation which is quite logical since the stl can't know anything about the internal data of an object.

The reason I asked was because it would be nice to mimic the behaviour 100% so that replacing a normal stl container with mine would require close to zero effort.

Share this post


Link to post
Share on other sites
Quote:

It actually works quite ok as is, i.e only allow writes to the whole object: test[2] = Foo(10, 10); but provide reading using the -> operator.

Yeah; but that's one of the "constraints" you'd have to apply (and something I'd find abhorrent since it's entirely unexpected).

Quote:

I have no problem running any of the algorithms that I've tried with my current implementation which is quite logical since the stl can't know anything about the internal data of an object....I'd like to mimic stl containers because I'd like to be able to use all algorithm's etc (and also to provide a well known interface).

But the SC++L algorithms work by way of the iterator mechanism, not the indexing operator -- observe that you can apply algorithms to an std::list, which does not supporting indexing whatsoever. That you can send instances of your SC++L class wrappers to various algorithms has nothing to do with whether or not your operator[] overload works properly/the way you want it to.

Furthermore, no SC++L container provides this "detect writes via operator[]" behavior. The only one that comes close (and is the odd man out in that respect) is std::map. It is often misunderstood to have "detect writes via operator[]" behavior because you can add new items to the container via someMap[someKeyThatDoesNotYetExist] = someValue;. However, std::map::operator[] is just testing for existence of the key and, if one is not found, creating it. The statement someMap[someKeyThatDoesNotYetExist]; will also add an element, even though no write has taken place.

I still don't see this as a useful feature. Can you provide a concrete example of some class you've created that would illustrate the utility of this approach? It looks like all you're trying to do is wrap the SC++L containers and provide this "notify on write" feature, which is not as useful as it might sound in general and is likely functionality that should be delegated to an object that can appropriately handle it anyway (unless you're planning to embed some kind of callback that users of the wrapper container can supply, instead of the simple member function you've shown in your examples; this would be slightly more useful, but still fundamentally ugly and overengineered).

Share this post


Link to post
Share on other sites
Quote:
But the SC++L algorithms work by way of the iterator mechanism, not the indexing operator -- observe that you can apply algorithms to an std::list, which does not supporting indexing whatsoever. That you can send instances of your SC++L class wrappers to various algorithms has nothing to do with whether or not your operator[] overload works properly/the way you want it to.

Yes I'm fully aware of that, the source above was the minimum I could write up that showed the situation.
The only difference is that instead of having a [] operator as above, my iterators have a dereference operator * that returns the proxy.. the "problem" is still the same however...
Quote:
Furthermore, no SC++L container provides this "detect writes via operator[]" behavior. The only one that comes close (and is the odd man out in that respect) is std::map. It is often misunderstood to have "detect writes via operator[]" behavior because you can add new items to the container via someMap[someKeyThatDoesNotYetExist] = someValue;. However, std::map::operator[] is just testing for existence of the key and, if one is not found, creating it. The statement someMap[someKeyThatDoesNotYetExist]; will also add an element, even though no write has taken place.

Yes I know, that's why I need to add support for that behaviour, of course I can throw stl out of the window, but that means I need to rewrite tons of code, using my own interface to containers etc, something that I try to avoid.
Keeping the same interface but adding a behaviour (detecting changes and do something when that happens) is what I want.
I know that the interface wasn't designed to do this, hence the trickery with the Proxy class above.
Quote:
I still don't see this as a useful feature. Can you provide a concrete example of some class you've created that would illustrate the utility of this approach? It looks like all you're trying to do is wrap the SC++L containers and provide this "notify on write" feature, which is not as useful as it might sound in general and is likely functionality that should be delegated to an object that can appropriately handle it anyway (unless you're planning to embed some kind of callback that users of the wrapper container can supply, instead of the simple member function you've shown in your examples; this would be slightly more useful, but still fundamentally ugly and overengineered).

Yes, I plan to be able to provide custom "callbacks" (functors), as for usage right now I wan't the following.
* Beeing able to log all changes made to data.
* Mirror containers over a network.
* Mirror containers to an external database.

All of the above can of course be easily done in other ways, I'd just like to use the same interface as SC++L, because it's well known and it's easier to retrofit when needed (only the container constructor and type needs to be changed).
For instance I can currently do the following:

db_map<std::string, Mp3Info> mp3s(dbConnection, "mp3s");
mp3s("Test.mp3") = ....;
db_map<std::string, Mp3Info>::iterator it = mp3s.find("Cool.mp3");
it->second = .....;

db_vector<std::pair<int, float> > stuff(dbConnection, "stuff");
stuff.push_back(std::make_pair(5, 10.0f));
stuff.push_back(std::make_pair(7, 20.0f));
stuff.push_back(std::make_pair(3, 30.0f));
std::sort(stuff.begin(), stuff.end());
stuff.erase(stuff.begin() + 1);
stuff.clear();




I currently supports four types of databases (MySql, PostGres, SqlLite and a "no dbase").
The key (if present) and values are serialized into columns in the database.
If the table already existed it's information is "loaded" into the container(upon construction).
I support all methods and operators of the SC++L containers that I've implemented (three so far, but it's easy to do more as needed).
I can use algorithm's on these containers and all changes a propagated to the database, it's rather nifty and I retrofitted a project of mine do store everything in databases very quickly.

Edit: Source tags, added samples and language :)

Share this post


Link to post
Share on other sites
Quote:

The only difference is that instead of having a [] operator as above, my iterators have a dereference operator * that returns the proxy.. the "problem" is still the same however...

Ah. That does make more sense, then.

Share this post


Link to post
Share on other sites
Sorry, if I sounds harsh, but sometimes it feels like people wants a complete copy of my source code base before they can answer a simple question :)
I guess I should have explained WHY and not only WHAT I wanted an answer...
Then there's me not having english as my native language, takes me forever (relatively) to write down my thoughts in english.
As you see the original question wasn't about trying to solve a problem, more a question out of curiosity.
I really didn't expect that there was any solution, but I've been wrong on such things a number of times before so I though I should ask anyway before rejecting the idea.
I'm still quite pleased on how things work now, there's a little bit of overhead needed, i.e:

Foo t = test[2];
t.m_bar = 77;
test[2] = t;

Works fine, albeit more expensive, than if you could write:

test[2].m_bar = 77;

Share this post


Link to post
Share on other sites
Often source code is the quickest and easiest way to explain what you are trying to do. [smile] Also it comes with advantage of being (at least partway) written.

Share this post


Link to post
Share on other sites
Quote:

Sorry, if I sounds harsh, but sometimes it feels like people wants a complete copy of my source code base before they can answer a simple question :)
I guess I should have explained WHY and not only WHAT I wanted an answer...

Well, you did omit, originally, a fairly critical bit of information (namely that operator[] was an example and that your real intention was to provide the described behavior for iterators as well), which caused the bulk of the disconnect. If you present a discussion about operator[] behavior, and then claim you're doing it to be compatible with an interface that doesn't use operator[], you should expect people to point out that operator[] has nothing to do with said interface. :D

I don't think English being a non-native language is a problem; what you've communicated you've communicated clearly. It was just the omission of the iterator bit.

Share this post


Link to post
Share on other sites
Quote:
Original post by eq
Then there's me not having english as my native language, takes me forever (relatively) to write down my thoughts in english.
Well congrats on doing such a good job that I bet most of us here couldn't tell.

Share this post


Link to post
Share on other sites
Quote:
Original post by jpetrie
Well, you did omit, originally, a fairly critical bit of information (namely that operator[] was an example and that your real intention was to provide the described behavior for iterators as well), which caused the bulk of the disconnect. If you present a discussion about operator[] behavior, and then claim you're doing it to be compatible with an interface that doesn't use operator[], you should expect people to point out that operator[] has nothing to do with said interface. :D

Point taken, I wish I hadn't used the [] operator as an example :)
But iterators really hasn't anything to do with it either.
Should have chosed a better topic, something like:
Quote:
Returning a "smart" reference to an object

Where this smart reference is coming from isn't importent (at least I don't think that it matters).

Quote:

I don't think English being a non-native language is a problem; what you've communicated you've communicated clearly. It was just the omission of the iterator bit.

So it's just me beeing stupid then ;)

Shouldn't even have asked in the first place, since it's quite obvious (giving it a bit more though) that it's impossible to intercept what's going on with the . operator in a statement like: foo.m_bar;

Even if you could overload the . operator what would the return be?
I'd like it to be of the type Proxy<type of m_bar, C>, but how would foo's . operator know wich type m_bar is?
Maybe a default operator like:

template <class R, class T> R& operator.(T& dataRef)
{
return dataRef;
}

Where the cpp compiler inserts a foo.m_bar as the dataRef parameter, then you could return something like this:

return Proxy<T, C>(m_c, dataRef, m_i);

Keep on dreaming... :)
Anyway, thanks for taking YOUR time to let ME realize what question I should have asked!
Edit: ..and for letting me know that I can communicate with half the world using the written word... amen for that. ;)

[Edited by - eq on May 9, 2007 3:42:08 PM]

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this