#include <map>
#include <utility>
template <typename Key, typename Value>
class another_map : public std::map<Key, Value> {
public:
using std::map<Key, Value>::map;
template <typename T>
// argument order is an open question
// I think value should be the first because of the proposed multi-indexed `operator[]`
// but the evaluation order should be also considered
Value& operator[]=(T&& value, const Key& key) {
auto result = insert_or_assign(key, std::forward<T>(value));
return *result.first;
}
};
int main() {
std::map<int, int> m{};
m[5] = 7; // m.operator[](5) = 7
another_map<int, int> am{};
am[5] = 7; // am.operator[]=(7, 5)
am[5] += 7; // am.operator[](5) += 7
int x = am[5]; // int x = am.operator[](5)
// some open-question case
(am[5]) = 7; // what should we do here?
}
I think value should be the first because of the proposed multi-indexed `operator[]` but the evaluation order should be also considered
`std::map`/`std::unordered_map` `operator[]` can not be fixed for compatibility reasons
Evaluation order for overloaded operators when defined works on the basis of the appearance of the expressions in code, not on the order of the parameters to the eventual function call. Assignment operators are ordered right-to-left, even though you could consider the first parameter of the member function to be `this` (ie: the left side).An `operator[]=` is an assignment operator; as such, it should order things like other assignment operators: right-to-left.
Here's something else that should be considered. This feature exists to optimize a particular use case of something that is actually more general in C++: access/assignment paring. That is, the user is conceptually accessing an object, but they're doing it for the purpose of performing assignment to the accessed object.The reason I suggest you consider this is that OutputIterators are fundamentally based on this. One of the most annoying aspects of writing a pure OutputIterator is that you have to kind of fudge the way its `operator*` works. If you cannot manifest an actual `value_type` to assign to, then you have to return a proxy object that references the meat of the iterator, which then uses its assignment operator to do the actual assignment.Avoiding that kind of thing would help make writing OutputIterators something that normal C++ programmers can idiomatically understand and implement.Your proposal would probably be stronger if it could solve this problem too, to allow `*it = foo;` and `*it++ = foo;` to magically transform into a single function call of the form `operator<whatever>(foo)`. And that's not going to be easy.Especially since the obvious `operator*=` is already taken ;)
`std::map`/`std::unordered_map` `operator[]` can not be fixed for compatibility reasonsI see no reason why this would have to be the case. As long as `some_map[key] = value` has the same behavior, it will be fine. After all, you still need the regular `operator[]` overload to make values accessible.Can you come up with a scenario where calling the `operator[]=` overload will have genuinely breaking behavior? Yes, it will call the copy/move constructor rather than a default constructor & copy/move assignment. But the rules of `map` already requires that these must be equivalent operations, right?
and `*it++ = foo;`
and `*it++ = foo;`I totally missed it. It is really hard to find a general solution with this three-operator case...
This looks like a use-case for operator dot ( or delegate base) proposal.
Nicol Bolas wrote:"Completely covered" is a highly dubious phrase. But let's assume that this is true and actually have this discussion.The first thing to realize is that OperatorDot is just a mechanism that makes it easier to generate proxy objects. It provides no real functionality which you could not emulate yourself right now just by writing a huge bunch of code. Indeed, if metaclasses ever becomes a thing, then C++ won't need OperatorDot at all.So the comparison here is really between the hypothetical `map::operator[]=` and a version of `map::operator[]` that returns a proxy.Now, remember the foundational example of this proposal: we want `some_map[key] = value;` to copy-construct the value inside of the map rather than performing default construction+copy-assignment. `operator[]=` accomplishes this directly.To do this with a proxy `operator[]`, it must return a prvalue object that acts like a `Value`. If you try to convert it into a `Value`, then it will default-construct the value. However, if you try to assign to it from a `Value`, then it will invoke copy/move construction on the uninitialized value.This means that, until the proxy object either is assigned to or invoked, the value at that key is not initialized. So... what happens here:auto &&obj = some_map[key];some_map[key] = some_value;In the current `map` type, this works just fine. `obj` is a reference to an initialized value of the map's `Value` type. The subsequent assignment assigns to the object that `obj` references. So later accesses to `obj` will see this value.A proxy-based version must emulate this. So... how do you do that? The proxy cannot just store a reference to the node. It has to know whether the `Value` in that node has been initialized or not, so that when someone accesses the proxy, it can know whether to perform initialization if needed. Such information cannot be local to the proxy itself, since code outside of that proxy can effect it. The data therefore must be part of the node itself.So you just gave the node type an additional byte of overhead (at least). Not to mention the additional cost of every piece of code that now needs to test whether the node has a value or not.But this also means that, between those two statements above, the state of `some_map` is... odd. There is an entry in the map which is unfinished. That is not a good state to leave the `map` in.Not only that, it now has to be implemented using some... legally dubious things, in terms of code. Now yes, implementing `std::map` already requires weirdness on the implementer's part, thanks to the ability to extract nodes from the `map`, modify their supposedly-`const` keys, and then reinsert them. But this is even more dubious. The node cannot store an actual `std::pair<Key, Value>`, since some nodes don't have valid values all the time.That's going to be pretty interesting to implement. And by "interesting", I mean "exceedingly obtuse expert-level coding".By contrast, `operator[]=` has no such problems. The node type doesn't need this extra "is the value initialized" information. The `map` is always in a perfectly legitimate state. And most importantly of all, implementing it correctly is way easier for the user. You don't have to write a proxy type. You don't have to add extra info to the node. You don't have to find a way to write a `pair<Key, Value>` that sometimes doesn't initialize the `Value`.You just add one function, and your code gets better.At the end of the day, OperatorDot is an expert-level feature. What is being discussed here is something anyone can do.Also, there is no way to back-port a proxy-based `operator[]` into `map`. Something as simple as:auto &val = some_map[key];Can't work in a proxy world (that's why I used `auto&&` above). And there are plenty of people who have written precisely that.By contrast, the only time an `operator[]=` would have different behavior is if default-construction+copy/move assignment is not functionally equivalent to copy/move construction for a given type. And at that point... I would say that your code deserves to be broken.
template <class Key, class Value>
struct MyMap : public std::map<Key, Value> {
struct Proxy : public using Value {
MyMap& map_ref;
Key const& key;
Proxy() = delete;
Proxy(Proxy const&) = delete;
Proxy& operator=(Proxy const& other) {
return *this = static_cast<Value&>(other);
}
// Standard proxy
operator Value&() const {
auto it = map_ref.find(key);
if (it == map_ref.end()) {
it = map_ref.emplace_hint(it, key, Value());
}
return it->value;
}
// Override operator=
Proxy& operator=(Value val) {
auto it = map_ref.find(key);
if (it == map_ref.end()) {
it = map_ref.emplace_hint(it, key, std::move(val));
return *this;
}
it->value() = std::move(val);
return *this;
}
};
Proxy operator[](Key const& key) {
return {*this, key};
}
};
"Completely covered" is a highly dubious phrase. But let's assume that this is true and actually have this discussion.The first thing to realize is that OperatorDot is just a mechanism that makes it easier to generate proxy objects. It provides no real functionality which you could not emulate yourself right now just by writing a huge bunch of code. Indeed, if metaclasses ever becomes a thing, then C++ won't need OperatorDot at all.
So the comparison here is really between the hypothetical `map::operator[]=` and a version of `map::operator[]` that returns a proxy.
Now, remember the foundational example of this proposal: we want `some_map[key] = value;` to copy-construct the value inside of the map rather than performing default construction+copy-assignment. `operator[]=` accomplishes this directly.
To do this with a proxy `operator[]`, it must return a prvalue object that acts like a `Value`. If you try to convert it into a `Value`, then it will default-construct the value. However, if you try to assign to it from a `Value`, then it will invoke copy/move construction on the uninitialized value.
This means that, until the proxy object either is assigned to or invoked, the value at that key is not initialized.
So... what happens here:auto &&obj = some_map[key];some_map[key] = some_value;
In the current `map` type, this works just fine. `obj` is a reference to an initialized value of the map's `Value` type. The subsequent assignment assigns to the object that `obj` references. So later accesses to `obj` will see this value.
A proxy-based version must emulate this. So... how do you do that? The proxy cannot just store a reference to the node. It has to know whether the `Value` in that node has been initialized or not, so that when someone accesses the proxy, it can know whether to perform initialization if needed. Such information cannot be local to the proxy itself, since code outside of that proxy can effect it. The data therefore must be part of the node itself.
So you just gave the node type an additional byte of overhead (at least). Not to mention the additional cost of every piece of code that now needs to test whether the node has a value or not.
But this also means that, between those two statements above, the state of `some_map` is... odd. There is an entry in the map which is unfinished. That is not a good state to leave the `map` in.
Not only that, it now has to be implemented using some... legally dubious things, in terms of code. Now yes, implementing `std::map` already requires weirdness on the implementer's part, thanks to the ability to extract nodes from the `map`, modify their supposedly-`const` keys, and then reinsert them. But this is even more dubious. The node cannot store an actual `std::pair<Key, Value>`, since some nodes don't have valid values all the time.
That's going to be pretty interesting to implement. And by "interesting", I mean "exceedingly obtuse expert-level coding".
By contrast, `operator[]=` has no such problems. The node type doesn't need this extra "is the value initialized" information. The `map` is always in a perfectly legitimate state.
And most importantly of all, implementing it correctly is way easier for the user. You don't have to write a proxy type.
You don't have to add extra info to the node. You don't have to find a way to write a `pair<Key, Value>` that sometimes doesn't initialize the `Value`.
You just add one function, and your code gets better.At the end of the day, OperatorDot is an expert-level feature. What is being discussed here is something anyone can do.
Also, there is no way to back-port a proxy-based `operator[]` into `map`. Something as simple as:auto &val = some_map[key];Can't work in a proxy world (that's why I used `auto&&` above). And there are plenty of people who have written precisely that.
By contrast, the only time an `operator[]=` would have different behavior is if default-construction+copy/move assignment is not functionally equivalent to copy/move construction for a given type. And at that point... I would say that your code deserves to be broken.
Especially since the obvious `operator*=` is already taken ;)
A proxy-based version must emulate this. So... how do you do that? The proxy cannot just store a reference to the node. It has to know whether the `Value` in that node has been initialized or not, so that when someone accesses the proxy, it can know whether to perform initialization if needed. Such information cannot be local to the proxy itself, since code outside of that proxy can effect it. The data therefore must be part of the node itself.You don't need extra information in the node, I just explained how to do that with a proxy.
So you just gave the node type an additional byte of overhead (at least). Not to mention the additional cost of every piece of code that now needs to test whether the node has a value or not.
No. see above.
You just add one function, and your code gets better.At the end of the day, OperatorDot is an expert-level feature. What is being discussed here is something anyone can do.Agreed. And I always agreed on that.But you cannot talk about operator []= without talking about operator dot, as one can be used to implement the same stuff as the other.
Also, there is no way to back-port a proxy-based `operator[]` into `map`. Something as simple as:auto &val = some_map[key];Can't work in a proxy world (that's why I used `auto&&` above). And there are plenty of people who have written precisely that.using auto = Value; might help (don't remember the proposal).But this is not a problem operator[]= vs operator dot, but a problem of operator dot by itself, which has to be solved anyway.
Oh, and here's another problem with proxies. Consider the following code:map<string, int> some_map;some_map["foo"s];By the rules of `map::operator[]`, it must default-construct that entry. But in the proxy-based version, the proxy is never accessed. So the only way to default-construct that entry is to do it in the proxy's destructor.Here's the thing though. `operator[]=` would have the same semantics as `insert_or_assign`. Namely, that `Value` does not need to be default constructible. Your `proxy::operator=` can provide similar behavior. But your `proxy::~proxy` cannot.
See, if `Value` is not default constructible, then in an `map::operator[]=` world, calling `map::operator[]` is a compile error. But with a proxy solution... what happens? You can't make calling `proxy::~proxy` a compile error for this case, because you cannot know at compile time if the user assigned to the proxy. It can be a runtime error, but throwing from a destructor is bad.So with the proxy based solution, if the `Value` is not default constructible, the best you can do is simply... not insert the element in the destructor. Even though that's what `operator[]` is supposed to do.Using indirect methods to simulate a direct action (proxies to simulate combined operations) is almost always faulty.On Sunday, March 17, 2019 at 6:04:52 AM UTC-4, floria...@gmail.com wrote:A proxy-based version must emulate this. So... how do you do that? The proxy cannot just store a reference to the node. It has to know whether the `Value` in that node has been initialized or not, so that when someone accesses the proxy, it can know whether to perform initialization if needed. Such information cannot be local to the proxy itself, since code outside of that proxy can effect it. The data therefore must be part of the node itself.You don't need extra information in the node, I just explained how to do that with a proxy.And your implementation of this is buggy. For example:auto &&obj = some_map[func()];
If `func()` returns a prvalue, that means the key will be destroyed at the end of this expression. Also, if `func()` returns a type implicitly convertible to `Key`, then the system will have to create a temporary... which will be destroyed at the end of the expression. So storing a reference to the `Key` is not going to work.Either you do it based on the node as I suggested, or your proxy must store a copy of the key. Neither option is a good one. The node one has all of the problems I suggested.The storing of a key in the proxy has the following problems:1. The proxy is now potentially much bigger and heavier weight. Keys sometimes allocate memory, so copying it is going to lead to a lot of extra memory allocations. Just consider a `map<string, int>`; the value is cheap, but the key is not. Yes, copying a proxy is not something that's likely to happen, since if you wanted a reference to the value, you would use a reference, not a value. But you still have to create the initial copy.2. Every access to the value through the proxy is an O(log(n)) operation. That is not acceptable. Oh sure, you could put a `Value*` in there which you initialize if the key comes back as being initialized. But now you're having to test that internal pointer, then test the return of `find`. That's a lot of conditional branching in something that ought to be trivial.There's no version of the proxy-based solution that's actually good; there are only varying degrees of bad.So you just gave the node type an additional byte of overhead (at least). Not to mention the additional cost of every piece of code that now needs to test whether the node has a value or not.No. see above.FYI: you don't have to respond to every single sentence or paragraph in my post. If you've already said something that makes a later point of mine moot, you can just ignore it.You just add one function, and your code gets better.At the end of the day, OperatorDot is an expert-level feature. What is being discussed here is something anyone can do.Agreed. And I always agreed on that.But you cannot talk about operator []= without talking about operator dot, as one can be used to implement the same stuff as the other.My problem is this. The proxy-based solution is harder to use, harder to implement correctly, harder to implement with equivalent performance in important cases, and cannot be dropped into existing data structures without creating backwards-incompatible changes.
Bringing up an alternative solution that is so obviously broken on so many levels feels like erecting a strawman: you're creating a fake adversary for the sole purpose of looking good by defeating it. Proxies are a sub-optimal solution; everyone knows its a sub-optimal solution, so why are we talking about them?
Also, there's the little problem that OperatorDot is dead. It's probably Uniform-Call-Syntax-levels of dead. Stroustrup was a major proponent of the idea, and it's not even on his list of priorities for C++ now, let alone the direction group's priorities. It's hard to say that an alternative exists when the alternative will likely never actually exist.
Also, there is no way to back-port a proxy-based `operator[]` into `map`. Something as simple as:auto &val = some_map[key];Can't work in a proxy world (that's why I used `auto&&` above). And there are plenty of people who have written precisely that.using auto = Value; might help (don't remember the proposal).But this is not a problem operator[]= vs operator dot, but a problem of operator dot by itself, which has to be solved anyway.But that's why I say that OperatorDot is irrelevant. All of the problems with the OperatorDot approach you outlined are problems that are fundamental to any proxy-based solution. It's like, there are 20 problems with using proxies for this in C++ as it currently stands; OperatorDot only solves one of those problems.I don't see how it's a genuine alternative if it still has the other 19 problems.
// *a = x;
T& operator*(std::op_simple_assignment_t, ...) { /* ... */ }
// a[k] = v;
T& operator[](std::op_simple_assignment_t, ...) { /* ... */ }
// a[i]++;
T operator[](std::op_post_increment_t, ...) { /* ... */ }
// a[i] |= x;
T& operator[](std::op_bitwise_or_assignment_t, ...) { /* ... */ }
Even more general could be to overload a complex expression which is currently only possible via expression templates and lazy evaluation technics. One might try to use operator overloading in a matrix caculation library:matrix a,b,c,d;
//...
a=b+c*d;// can I overload this too?regards,
FM