operator[]

Started by
9 comments, last by eq 16 years, 11 months ago
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;
	}
	typedef Proxy<T, MyVec<T> > P;
	P operator[](const unsigned int i)
	{
		return P(this, m_v, 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?
Advertisement
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.
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.
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).
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 :)
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.
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;
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.
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.
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.
"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms

This topic is closed to new replies.

Advertisement