Bug 7753 – Support opIndexCreate as part of index operator overloading in user-defined types

Status
NEW
Severity
enhancement
Priority
P4
Component
dmd
Product
D
Version
D2
Platform
All
OS
All
Creation time
2012-03-22T21:18:10Z
Last change time
2024-12-13T17:59:16Z
Assigned to
No Owner
Creator
hsteoh
Moved to GitHub: dmd#18429 →

Comments

Comment #0 by hsteoh — 2012-03-22T21:18:10Z
Currently, a nested indexing expression such as: a[b][c][d] = 0; for a user-defined type that overloads opIndex* gets translated into: a.opIndex(b).opIndex(c).opIndexAssign(0,d); However, if a[b][c] do not yet exist, this will fail. This works correctly for built-in associative arrays, because the expressions get translated into a series of calls to _aaGetX(), which creates new entries if they don't already exist. But currently, there is no way for a user-defined type to accomplish the same thing. Suggested fix: if the expression as a whole is being assigned to with an assignment operator, then the upper-level indexing calls should be translated into opIndexCreate() instead of just opIndex(): a[b][c][d] = 0; becomes: a.opIndexCreate(b).opIndexCreate(c).opIndexAssign(0,d); If opIndexCreate is not defined, then replace it with opIndex (for backward compatibility). The semantics of opIndexCreate(k) is to return the entry indexed by k if it exists, and if it doesn't, create a new entry with key k and the .init value of the value type, and return the new entry. Preferably, this will apply to any expression that ends with a call to opIndexAssign, opIndexUnary, and opIndexOpAssign. But at the very least, this needs to work when the expression ends with opIndexAssign.
Comment #1 by dmitry.olsh — 2012-03-23T03:28:52Z
It might be a good thing, but ... Why not just return a proxy type upon each indexing? The proxy type will have createIndex that will forward to others in turn. Here a prof of concept I belive it could be generalized and polished. For simplicity sake it's for n-dim arrays: import std.stdio, std.exception; struct Proxy(T) { T* _this; int idx; void opAssign(X)(X value){ debug writeln("Proxy.opAssign"); createIndex(idx) = value; } static if(typeof(*_this).dimension >= 2) { // somewhere io expression ...a[idx][jdx]... is create all, except last one auto opIndex(int jdx){ return proxy(&_this.createIndex(idx), jdx); } //a[idx][jdx] = y; is create if non-existent auto opIndexAssign(X)(X val, int jdx){ //TODO: constraints! debug writeln("Proxy.opIndexAssign"); _this.createIndex(idx).createIndex(jdx) = val; } } @property ref expr(){ debug writeln("Proxy.expr"); return _this.normalIndex(idx); } alias expr this; } auto proxy(T)(T* x, int idx){ return Proxy!(T)(x,idx); } struct M(size_t dim) { static if(dim == 1){ alias int Val; } else{ alias M!(dim-1) Val; } enum dimension = dim; Val[] arr; ref createIndex(int idx){ debug writeln("Created ", typeof(this).stringof); if(arr.length < idx) arr.length = idx+1; return arr[idx]; } ref normalIndex(int idx){ debug writeln("Indexed ", typeof(this).stringof); return arr[idx]; } auto opIndex(int idx){ return Proxy!(M)(&this, idx); } alias arr this; } unittest{ M!(3) d3arr; d3arr[1][2] = [2, 3, 4]; assert(d3arr[1][2][2] == 4); int[] x = d3arr[1][2]; assert(d3arr[1][2].length == 3); assert(d3arr[1][1] == null); //inited //booom used before explicit = assert(collectException!Error(d3arr[2][2][1] + 1 == 1) !is null); }
Comment #2 by hsteoh — 2012-03-23T07:36:46Z
That's a pretty neat idea. Can it be made to work with containers that contain other containers (possibly of a different type)? E.g., a linked list of arrays of AA's?
Comment #3 by dmitry.olsh — 2012-03-23T08:17:58Z
Well, linked list is, for sure, not indexed so not a problem ;) As for sets I don't see a problem, I can extend this idea to arbitrary set easily. In fact it's even cleaner for sets (maps) then arrays. If you need a headstart I can scratch up a simple version for integer sets.
Comment #4 by dmitry.olsh — 2012-03-23T08:22:00Z
Ahm. So Q was about geterogenious stuff like arrays of sets(maps) or maps of arrays ? I think the 2 mentioned situations cover it all, thus you can parametrize this idea on basis of: a) contigous container, to get item with index X you need to allocted all elements up X. Here X is obviously can be only integer of some sort. b)non-contigous container, to get item with index X you check/create only slot indexed by X.
Comment #5 by dfj1esp02 — 2015-05-28T08:59:21Z
Whoa, this feature is weird indeed. Given the declaration int[int][int] a; a[0][0]=0 shouldn't work because the AA has no entry with key 0, so a[0] should throw RangeError similar to how b=a[0] throws RangeError.
Comment #6 by code — 2015-05-29T11:25:17Z
Igor Stepanov suggested [¹] an opIndex extension. ref Foo opIndex(bool lvalue)(size_t idx) Where the compiler would call opIndex!true when an lvalue is required and opIndex!false when an rvalue suffices. [¹]: http://forum.dlang.org/post/[email protected]
Comment #7 by code — 2015-05-29T17:47:36Z
(In reply to Martin Nowak from comment #6) > Igor Stepanov suggested [¹] an opIndex extension. > > ref Foo opIndex(bool lvalue)(size_t idx) A separate opIndexCreate might be better, b/c it allows you to have a ref return for one and an rvalue return for the other function. It also allows to use those operands as polymorphic functions in classes.
Comment #8 by schveiguy — 2020-02-27T21:27:10Z
Just wanted to post something here. The call a[b][c][d] = 0 results in different calls (_aaGetY now) than x = a[b][c][d] (_aaGetRvalueX). So I think H S Teoh is onto the right path. Having a different opIndex available for assignment (or lvalue manipluation) makes sense and should be straightforward to define. Now, to define this a little more concretely, I think opIndexCreate should ONLY be used when the entire chain of indexing results in a definite lvalue requirement. This means ONLY opIndexCreate (or AA usage) available in the expression, and the final "call" should be an opAssign or opOpAssign or opIndexOpAssign. I would also throw in opUnary that expects mutation (i.e. ++ or --) because it's currently supported by AAs. Unfortunately, the existing behavior is somewhat inconsistent: struct S { int x; void opUnary(string s : "++")() {++x;} } S[int][int] aa; aa[1][1] = S(1); // ok aa[2][2].x = 5; // range violation ++aa[3][3]; // range violation int[int] aa2; aa2[4] += 3; // ok ++aa2[5]; // ok void foo(ref int x) foo(aa2[6]); // range violation So there is not 100% consistency here. The most rational logical implementation would just require lvalue usage. But the reality is different. Also note that ++aa[5] does not match ANY operator that would be on the type of aa. There is no opIndexOpUnary akin to opIndexOpAssign. And it doesn't work if your underlying type supports ++. That is an inconsistency that will be tough to duplicate.
Comment #9 by robert.schadek — 2024-12-13T17:59:16Z
THIS ISSUE HAS BEEN MOVED TO GITHUB https://github.com/dlang/dmd/issues/18429 DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB