0

EDIT I caused some confusion by referring to rvalues. I provide an example of what I want to do at the bottom.

I want to separate these two calls:

obj[key] = value;
value = obj[key];

I tried using two operator[] overloads, hoping the const method would be used exclusively for rvalues, and the non-const version would be exclusively used for lvalue calls.

const ValueType& operator[] (const KeyType& key) const;
ValueType& operator[] (const KeyType& key);

But my const version was only ever called when this was const.

I tried using a wrapper that would override op= and op ValueType.

BoundValue<KeyType, ValueType> operator[] (const KeyType& key)

// later in BoundValue
operator ValueType() const
const V& operator= (const V& value)

This worked marvelously...until I hoped op[] would implicitly cast for a template argument deduction (which is not allowed).

// example failure of wrapper
// ValueType is string, std::operator<< from string is a template method
cout << obj[key];

For this specific case, I could define my own ostream methods.

ostream& operator<< (ostream& os, const BoundValue<K, V>& bv);

But my type would similarly fail in any user defined template argument (seems likely).

I can think of two work arounds, both of which greatly reduce the sweetness of the syntax sugar I am trying to provide:

// example where ValueType is string
cout << static_cast<string>(obj[key]);

or,

// added to BoundValue
const ValueType& Cast() const

// later in use
cout << obj[key].Cast();

Example of desired use case

Map<int, string> m;
m[1] = "one";

try
{
    string one = m[1];
    cout << "one is set: " << one << endl;
    string two = m[2];
    cout << "two is set: " << two << endl;
}
catch (...)
{
    cout << "An exception was thrown" << endl;
}

Expected output:

one is set: one
An exception was thrown

And how/why is the exception thrown? I can check if the key exists, and throw if it doesn't. If I can't separate the get from the set, I don't know when access for a key is allowed.

payo
  • 4,501
  • 1
  • 24
  • 32
  • The usage example with `obj[key]` has absolutely nothing to do with what I think you're thinking about. The difference is between `obj[key]` and `foo()[key]`. – Kerrek SB Mar 12 '14 at 00:50
  • Provide an example that we can run or see please? I cannot understand what error you are trying to overcome.. You can of course template `operator <<` So providing a template argument cannot be the problem? Unless of course I misunderstand greatly.. which I think is the case.. – Brandon Mar 12 '14 at 01:02
  • In C++ an expression is evaluated on its own merit, without reference to its context... `obj[key]` has to have a data type that is the same regardless of context, and whether or not it is an lvalue depends solely on what data type your `operator[]` returns. – M.M Mar 12 '14 at 01:57
  • Something else to bear in mind is that `obj[key]` has to create a new entry if `key` is an unknown key, either that or throw an exception. With `std::map`, if you want to do a pure get, you have to use the `find` function or otherwise. – M.M Mar 12 '14 at 02:34
  • @MattMcNabb I understand expression evaluation - but also accept there may be some tricks I haven't thought of. I also am aware of std map. I am trying to provide some syntax sugar that std map is not providing. boost showed us c++ can do many things we may not have expected to be possible without the compiler features now part of c++. – payo Mar 12 '14 at 03:38
  • @MattMcNabb also, (as you said) map[key] has to create a new key when key doesn't exist - but I want to know when this is an assignment, and not simply a read only operation. When it is an assignment, I'll happily create the key dynamically. But during reads, I want to fault. std map doesn't do this, it only supports this readonly behavior via at(), and op[] creates a key when one didn't exist. I'm trying to improve upon this model. – payo Mar 12 '14 at 03:42
  • My point is that you can't; `map[key]` has to be evaluated independent of what the user does with it. That's how C++ works. – M.M Mar 12 '14 at 04:07

2 Answers2

1

operator[] returns a reference(-like) type which implements dereference (get) and mutation (set) operations, operator*() and operator=(). In theory, at least, you're not required to return a real reference, as long as you're willing to do the work required to make the value you return act (mostly) like a reference. So you could use a type which stored a pointer to a pair<const Key, Val> in the case of a key which exists in the map, and a copy of the the non-existent Key otherwise. In the latter case, the dereference operator would thrown an exception and the mutation operator would move the key into the map.

Keeping a copy of keys in order to lazily add them to the map if necessary is a bit expensive, but it's not necessarily outrageous if you really want the functionality. One possible optimization would be to always add the key to the map but without constructing the matching Val type, and also without allowing the map to return the Key value. An easy implementation approach would be to allocate a single bit per key-value pair, indicating whether or not the key is actually in the map. As before, an attempt to dereference a pointer to a key-value pair not really in the map would raise an exception, while a mutation of the value would simply construct the new value and set the valid bit. The destructor of the reference operator would actually remove the invalid key from the map, but the assumption is that this operation would rarely occur since it would be, by definition, exceptional. Unfortunately, a robust implementation is not that simple because you need to consider the possibility of the simultaneous existence of more than one reference object corresponding to the same non-existent key. If the value type is large enough that a reference count could be overlaid on it, then a reference counting scheme is possible, but I think that you need to worry about data races unless your operator[] is carefully documented. (The reference count is not quite trivial because it is possible for the key to become valid as a result of one of the references being mutated. Fortunately, there is no need to maintain the count for a valid key, so the destructor merely needs to check for validity before decrementing the reference count and/or removing the invalid key.)

There are a couple of weaknesses with the above approach. One is that C++ does not have a single mutation operator; it actually has quite a variety (+=, -=, <<=, etc., etc., and not forgetting ++ and --). So there's going to be quite a bit of boilerplate.

More seriously, it is totally legitimate for a program to retain the value returned by operator[] as a reference object, and many programmers will assume that the reference returned is a simple Val&. Typically, the intent of such code is to be able to perform multiple mutations without performing multiple lookups. Aside from the type issues, easily solved these days with auto, that will "probably work", but the lazy insertion might create some surprises.

rici
  • 234,347
  • 28
  • 237
  • 341
  • I actually want this approach, and began to do it with my `BoundValue` type. The race conditions I've delt with and found them to be extremely interesting and rewarding (I've built a similar project in C#). `BoundValue` would contain all the special operator handlers, as you suggest. My problem comes when `BoundValue` (or `pair` in your case) needs to be cast implicitly. User defined casts are not run in template argument deduction. – payo Mar 12 '14 at 04:24
0

Did you try:

     Friend ostream& operator<< (ostream& os, const BoundValue<K, V>& bv);

when a operator is overloaded in another class and you want to overload the operator in your class you must declare your function as a friend function. Like when using operator << which is overloaded in "fstream "library

Russell Jonakin
  • 1,716
  • 17
  • 18
  • Thank you for your information. Getting a type to print to the ostream is not the issue. I am trying to separate get and set access on an indexer - to know how the user is trying to use the [] operator. Using the BoundValue I was able to delay evaluation, but it doesn't work for templated arguments due to implicit casting. Yes I can provide specializations to template methods such as this from osteam, but it doesn't solve the overall problem. – payo Mar 12 '14 at 03:40