I have a container class called Properties
. I want to add to it operator[](const std::string & name)
which will return property with specified name.
Now lets consider that there is no property with specified name. In this case I wan't to add new Property
with specified name to my Properties
if it is used as l-value and throw exception otherwise.
Properties pts;
pts.add("name1", val1);
pts.add("name2", val2);
pts["name1"] = val3; //OK
pts["name3"] = val1; //OK creating new Property with value = val1
cout << pts["name4"]; //Ooops can't find Property with name = "name4", so throwing an exception
Is this possible in C++? How can I write such operator[]
?
You can cover the cases you give, but not by actually checking whether lvalue-to-rvalue conversion occurs. I don't think it's possible to directly intercept that, so you need to provoke a different conversion instead:
operator[]
returns a proxy object, as John Zwinck says. Just creating this proxy object doesn't create the key.
The proxy object has an operator=(const V&)
, so that handles assignment by creating the key. Optionally you could also have operator+=
, operator++
and the rest - I'm not sure whether you mean that any lvalue use is OK, or just straight assignment.
The proxy object has a conversion to V&
which throws if the key doesn't already exist.
Edit: this seems to vaguely work, although there are use-cases it doesn't cover, such as passing the return value of operator[]
to a function that takes V&
and assigns to it in there. Also, hiding a proxy+conversion never results in a precisely equivalent interface to what you'd have with the original type, because implicit conversions can involve at most one user-defined conversion, and the proxy "uses up" that conversion.
#include <iostream>
#include <map>
#include <string>
#include <stdexcept>
struct FunnyMap;
struct ProxyValue {
FunnyMap *ptr;
std::string key;
ProxyValue(const std::string &key, FunnyMap *ptr) : ptr(ptr), key(key) {}
operator int&();
int &operator=(int i);
};
struct FunnyMap {
std::map<std::string, int> values;
ProxyValue operator[](const std::string &key) {
return ProxyValue(key, this);
}
};
ProxyValue::operator int&() {
if (ptr->values.count(key) != 0) {
return ptr->values[key];
} else {
throw std::runtime_error("no key");
}
}
int &ProxyValue::operator=(int i) {
return ptr->values[key] = i;
}
void foo(int &i) {
i = 4;
}
int main() {
try {
FunnyMap f;
f["foo"] = 1;
std::cout << f["foo"] << "\n";
std::cout << f["bar"];
// foo(f["bar"]); // also throws
} catch (const std::exception &e) {
std::cout << "Exception: " << e.what() << "\n";
}
}
Output:
1
Exception: no key
You could have your operator[]
return a proxy object rather than a plain reference to the contained value. Then you could set a flag in the proxy when it is assigned to (i.e. when the proxy's operator= is called). Then you could throw in the proxy's destructor if it was never assigned. Of course you'll need to instantiate the proxy with a boolean telling it whether to require assignment (no value existed) or not (a value was already set).
I'll note that it is usually considered bad practice to throw in a destructor. In particular, the proxy's destructor should not throw if it was called due to another exception (e.g. an error between key lookup and value assignment). You'd probably want to skip throwing from the proxy's destructor if an exception is already in flight, and you can detect this condition with std::uncaught_exception()
.
And finally I will reference this article about uncaught_exception()
:
http://www.gotw.ca/gotw/047.htm
It makes two arguments against using this function. The first is that it may sometimes return true when it is actually safe to throw. I claim that we can live with this in your case because we are trying to provide a safety check and if we sometimes fail to provide the safety check then we are not much worse off than before. And if we can agree that sometimes not having the check be done is OK in your case, then the second argument (the "moral" one) in the article can also be ignored (because your proxy will not have two different error handling mechanisms, it will have one which is usually effective but not always).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With