operator[]= (array subscript assignment operator)

1,763 views
Skip to first unread message

wpmgpro...@gmail.com

unread,
Mar 14, 2019, 8:59:28 PM3/14/19
to ISO C++ Standard - Future Proposals
There are a lot of programming languages that support this feature (D, C#, Python, etc.). It is also likely that it will be adopted in Rust language (I mention it because it is the most related to C++ language in my opinion), see https://github.com/rust-lang/rfcs/issues/997.

This operator does not break backward compatibility: operator[]= will only be called if it is defined. The open question: should we call `operator[]` as fallback if no appropriate `operator[]=` can be found but some `operator[]=` is provided.

Usage example:
#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?
}

Also it will be good if non-const `operator[]` will be removed from the proposed `std::flat_map` container adaptor, so we could define `operator[]=` for it in the future without breaking backward compatibility. But yeah, of course there will be inconsistency between `std::map` (and `std::unordered_map`) and `std::flat_map` behavior. I hope we can at least remove non-const `operator[]` for `std::flat_map` without defining `operator[]=`, so it would be more safe to use `std::flat_map`.

Possible flaws:
  • Complexity for rarely used feature? (not so rare as chained comparisons, but?)
  • Inconsistency with the current `operator[]` design in associative containers?
  • `std::map`/`std::unordered_map` `operator[]` can not be fixed for compatibility reasons, so it would be still headache for C++ beginners and now inconsistency headache because of different behavior between old and new associative containers?
  • First odd composite operator?

Nicol Bolas

unread,
Mar 15, 2019, 11:33:20 AM3/15/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
I think value should be the first because of the proposed multi-indexed `operator[]` but the evaluation order should be also considered

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 reasons

I 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?

wpmgpro...@gmail.com

unread,
Mar 15, 2019, 7:42:06 PM3/15/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
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.

Thanks for clarifying that.

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 ;)

Thanks for the nice suggestion! The only good idea about operator syntax that come to my mind is `operator*operator=` and more generally `operator$operator@` where $ can be * or [] and @ is one of =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ++, --. `operator[]operator|=` and similar would fit good for `std::vector<bool>`/`std::bitset`-like containers and for their iterators.

`std::map`/`std::unordered_map` `operator[]` can not be fixed for compatibility reasons

I 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?

I was talking primarily about current `operator[]` behavior for non-existing entries (creating entries with default-constructed values instead of throwing exceptions). But you are right, adding `operator[]=` (or `operator[]operator=`) overload for `std::map` should not break the current behavior.
Message has been deleted

wpmgpro...@gmail.com

unread,
Mar 16, 2019, 3:48:50 AM3/16/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
and `*it++ = foo;`

I totally missed it. It is really hard to find a general solution with this three-operator case...

Nicol Bolas

unread,
Mar 16, 2019, 9:37:33 AM3/16/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
On Saturday, March 16, 2019 at 3:48:50 AM UTC-4, wpmgpro...@gmail.com wrote:
and `*it++ = foo;`

I totally missed it. It is really hard to find a general solution with this three-operator case...

It's still a 2 operator case. The ++ is evaluated before the `*` and the `=`. It's no different from `*some_func() = foo;`, or `some_func()[10] = foo;`. It's all about recognizing the sequence of evaluating a left-hand `*` or `[]` followed by an `=` evaluation.

floria...@gmail.com

unread,
Mar 16, 2019, 11:52:09 AM3/16/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
This looks like a use-case for operator dot ( or delegate base) proposal.

Nicol Bolas

unread,
Mar 16, 2019, 12:02:10 PM3/16/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com, floria...@gmail.com
On Saturday, March 16, 2019 at 11:52:09 AM UTC-4, floria...@gmail.com wrote:
This looks like a use-case for operator dot ( or delegate base) proposal.

I fail to see how those proposals matter relative to this one. Operator-Dot proposals just make it easier to write proxies. The point of this idea is to not have proxies to begin with, to combine two operations into a single function call.

floria...@gmail.com

unread,
Mar 16, 2019, 1:52:03 PM3/16/19
to ISO C++ Standard - Future Proposals
If we had proxies, then would be able to implement all those. But they also would be used for extra use cases.

This proposal use-case would be completely covered by operator dot proposals, but not the other way around, so they do matter in the discussion.
Message has been deleted
Message has been deleted
Message has been deleted

floria...@gmail.com

unread,
Mar 17, 2019, 5:15:44 AM3/17/19
to ISO C++ Standard - Future Proposals
Apparently, Google groups refuses to post Nicol's reply, so I paste it completely here:
My reply will follow into another post
 
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.
Message has been deleted

floria...@gmail.com

unread,
Mar 17, 2019, 6:04:52 AM3/17/19
to ISO C++ Standard - Future Proposals
First, I will show a possible implementation using "Smart reference through delegation because that's the one I know best.

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};
 
}
};


Also, please remember I never said that operator[]= was useless if we had operator dot (or similar), but only that operator[]= features are already covered by operator dot.
But I completely agree that it is easier to write operator[]= than a proxy.
 
"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.

It does not only makes easier to generate proxy objects, it does enable the generation of proxy objects within generic codes. It is currently not possible to do it if you don't know the type you have to proxyfy.
 

So the comparison here is really between the hypothetical `map::operator[]=` and a version of `map::operator[]` that returns a proxy.

Agreed.
 

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.

Agreed. Please take a look at the example I provided at the beginning of this post on how to implement it with proxies.
 

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.

No, you can "override" an operator from the proxy to change the behavior of the operator (or a method): see my example.
 

This means that, until the proxy object either is assigned to or invoked, the value at that key is not initialized.

Agreed.
 
So... what happens here:

auto &&obj = some_map[key];
some_map[key] = some_value;

Simple: obj is a proxy, so does not generate the value at key, but when some_map[key] = is evaluated, it generates the node at the key with some_value in-place (no default + copy).
In fact, it works better than operator[]= in that case.
And later use of obj will find the right node because operator Value& is called on the fly whenever you need it.
 

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.

No problem here, as I just explained, it will behave the same.
 

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.
 

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.

No, there is no "odd" state of the map. The node is either non-existent, or completely built.
In your code, it is just that after auto&& obj = some_map[key]; the node is not yet created, so if you try to find it, you will get no node at all.
 

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.

See above: no change of the inner structure of the map.
 

That's going to be pretty interesting to implement. And by "interesting", I mean "exceedingly obtuse expert-level coding".

You're overdoing it here.
 

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.

No, see above
 
And most importantly of all, implementing it correctly is way easier for the user. You don't have to write a proxy type.

Agreed.
 
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`.

This is not needed, 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.
 

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.

Agreed.

Vinnie Falco

unread,
Mar 17, 2019, 9:27:04 AM3/17/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com

On Friday, March 15, 2019 at 8:33:20 AM UTC-7, Nicol Bolas wrote:
Especially since the obvious `operator*=` is already taken ;)

A straightforward solution is to introduce a std tag type:


    T& operator*=(std::op_assign_equal, ...);

Regards
Vinnie

Nicol Bolas

unread,
Mar 17, 2019, 12:03:52 PM3/17/19
to ISO C++ Standard - Future Proposals, floria...@gmail.com
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.

floria...@gmail.com

unread,
Mar 17, 2019, 12:40:38 PM3/17/19
to ISO C++ Standard - Future Proposals
I never said operator dot was better.

But to consider a proposal, you must consider alternatives, and operator dot is an alternative.
So I mentionned operator dot in order to compare the operator[]= proposal to alternatives and see what is better on each sides.

On Sunday, March 17, 2019 at 5:03:52 PM UTC+1, Nicol Bolas wrote:
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.

This (and the rest of your post) is actually a real problem of operator dot compared to operator[]=.
 

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()];

There is a proposal to fix those kind of lifetime issues (unrelated to proxies), but I'm unable to find it.
 

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.

Agreed.
 

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?

Because it is a more generic solution (it is not only about operator[]), so if at some point operator dot is part of the language (with most of the issues you pointed out that can be solved, solved), would it still make sense to integrate operator[]= ?
You showed some good reasons why it should, fair enough.

Please keep in mind that operator dot is a sub-optimal solution only in its current state, but most issues are actually solvable.
 

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.

In the past few months, I encountered a couple of case where I actually needed good proxies. I cannot think that I'm the only one in the world in that case.
It has lost interest, that's for sure.
But if proxies are implemented with metaclasses, that would still be an alternative to operator[]=. So the alternative to operator[]= is not really operator dot, but anything that can actually implement generic proxies.
 

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.

Because most of these problems will eventually find a solution (or C++ will be forever a broken language).

Thomas Köppe

unread,
Mar 17, 2019, 3:00:14 PM3/17/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
A few thoughts on proxies, in a modern light:
  • We should probably have "proxy invalidation" rules. It seems tricky to implement something like looking up an existing value into a proxy, then allowing an interleaving deletion of that element, and then still letting that proxy do something useful.
  • That said, the proxy could have a two-fold nature: if the key was found, it stores only the relevant iterator. If the key was not found, it stores a special kind of node-handle where the mapped-element isn't constructed yet, along with relevant hint information (a lower-bound iterator for ordered maps, and a precomputed hash for unordered maps). Having the node-handle solves the problem of retaining the key for later use without making copies or having dangling references. Upon assignment, the mapped-element is constructed in-place and the node handle is inserted.

Artem Golubikhin

unread,
Mar 17, 2019, 7:08:28 PM3/17/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
Sorry, I misunderstood you. I thought there is a need in overriding behavior for `*it++ = foo;` (turning three ops into a single function call) case too.

A̶b̶o̶u̶t̶ ̶s̶y̶n̶t̶a̶x̶,̶ ̶a̶n̶o̶t̶h̶e̶r̶ ̶i̶d̶e̶a̶ ̶c̶o̶m̶e̶ ̶t̶o̶ ̶m̶y̶ ̶m̶i̶n̶d̶:̶ ̶w̶e̶ ̶c̶a̶n̶ ̶i̶n̶t̶r̶o̶d̶u̶c̶e̶ ̶n̶e̶w̶ ̶c̶o̶n̶t̶e̶x̶t̶-̶s̶e̶n̶s̶i̶t̶i̶v̶e̶ ̶k̶e̶y̶w̶o̶r̶d̶ ̶`̶a̶c̶c̶e̶s̶s̶`̶ ̶(̶o̶r̶ ̶m̶a̶y̶b̶e̶ ̶`̶a̶c̶c̶e̶s̶s̶o̶p̶`̶,̶ ̶`̶a̶c̶c̶e̶s̶s̶o̶p̶e̶r̶`̶,̶ ̶`̶a̶c̶c̶e̶s̶s̶_̶o̶p̶e̶r̶a̶t̶o̶r̶`̶)̶,̶ ̶s̶o̶ ̶i̶n̶s̶t̶e̶a̶d̶ ̶o̶f̶ ̶`̶o̶p̶e̶r̶a̶t̶o̶r̶[̶]̶ ̶o̶p̶e̶r̶a̶t̶o̶r̶=̶`̶ ̶i̶t̶ ̶w̶i̶l̶l̶ ̶b̶e̶ ̶`̶o̶p̶e̶r̶a̶t̶o̶r̶=̶ ̶a̶c̶c̶e̶s̶s̶[̶]̶`̶.̶ ̶H̶m̶,̶ ̶o̶r̶ ̶t̶h̶e̶ ̶k̶e̶y̶w̶o̶r̶d̶ ̶c̶a̶n̶ ̶b̶e̶ ̶`̶a̶s̶s̶i̶g̶n̶m̶e̶n̶t̶`̶ ̶a̶n̶d̶ ̶t̶h̶e̶ ̶n̶a̶m̶e̶ ̶o̶f̶ ̶o̶p̶e̶r̶a̶t̶o̶r̶ ̶w̶i̶l̶l̶ ̶b̶e̶ ̶`̶o̶p̶e̶r̶a̶t̶o̶r̶[̶]̶ ̶a̶s̶s̶i̶g̶n̶m̶e̶n̶t̶=̶`̶.̶ ̶O̶r̶ ̶m̶a̶y̶b̶e̶ ̶e̶v̶e̶n̶ ̶`̶o̶p̶e̶r̶a̶t̶o̶r̶ ̶a̶c̶c̶e̶s̶s̶_̶o̶p̶e̶r̶a̶t̶o̶r̶[̶]̶ ̶a̶s̶s̶i̶g̶n̶m̶e̶n̶t̶_̶o̶p̶e̶r̶a̶t̶o̶r̶=̶`̶ ̶(̶h̶o̶w̶e̶v̶e̶r̶ ̶+̶+̶ ̶a̶n̶d̶ ̶-̶-̶ ̶d̶o̶n̶'̶t̶ ̶f̶i̶t̶ ̶g̶o̶o̶d̶ ̶w̶i̶t̶h̶ ̶`̶a̶s̶s̶i̶g̶n̶m̶e̶n̶t̶_̶o̶p̶e̶r̶a̶t̶o̶r̶`̶ ̶p̶h̶r̶a̶s̶e̶)̶.̶

That is a nice idea. Though I think it should be used another way, so no any new syntax like `operator[]=` is introduced:
// *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, ...) { /* ... */ }

On Sunday, 17.03.2019 г., 12:42:27 UTC+3 Farid Mehrabi wrote:
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


I think it is not for this proposal. This proposal should cover only simple access-and-modify operations for containers and iterators as for me. I want to leave the proposed idea as simple as it can be, but of course without breaking forward compatibility for the possible more complex composite operators proposal in the future.



Also I realized that the proposed feature doesn't cover the full functionality of `std::bitset`/`std::vector<bool>` reference proxies. There is `flip` member function in bool (bit) reference proxies, instead we can do `bits[i] ^= true;`, but `bits[i].flip();` looks more readable. Though `bits[i] = !bits[i];` looks readable too however it doesn't look DRY.
Message has been deleted
Message has been deleted

Artem Golubikhin

unread,
Mar 22, 2019, 6:55:06 PM3/22/19
to ISO C++ Standard - Future Proposals, wpmgpro...@gmail.com
We might also want to have `operator[](std::op_sizeof_t, ...)` for the `std::bitset`/`std::vector<bool>` case to prohibit using `sizeof(operator[expr])`. Just like we cannot use `sizeof` on bit-fields. The same for `operator*` in `std::bitset`/`std::vector<bool>` iterators case.
Reply all
Reply to author
Forward
0 new messages